From 098e2d2c1ac353a45e8f491f55ac1780e8466049 Mon Sep 17 00:00:00 2001 From: Guo XIn <371864209@qq.com> Date: Tue, 7 Nov 2023 15:42:48 +0800 Subject: [PATCH] first commit --- .gitignore | 38 + .mvn/wrapper/maven-wrapper.jar | Bin 0 -> 58727 bytes .mvn/wrapper/maven-wrapper.properties | 2 + mvnw | 316 ++ mvnw.cmd | 188 + package.bat | 2 + pom.xml | 69 + .../AbstractAnnotationSynthesizer.java | 168 + .../AbstractLinkAnnotationPostProcessor.java | 163 + .../AbstractWrappedAnnotationAttribute.java | 71 + .../core/annotation/AggregateAnnotation.java | 27 + .../java/cn/hutool/core/annotation/Alias.java | 26 + .../AliasAnnotationPostProcessor.java | 66 + .../cn/hutool/core/annotation/AliasFor.java | 39 + .../AliasLinkAnnotationPostProcessor.java | 126 + .../AliasedAnnotationAttribute.java | 36 + .../core/annotation/AnnotationAttribute.java | 104 + .../AnnotationAttributeValueProvider.java | 18 + .../core/annotation/AnnotationProxy.java | 88 + .../annotation/AnnotationSynthesizer.java | 77 + .../core/annotation/AnnotationUtil.java | 576 +++ .../CacheableAnnotationAttribute.java | 63 + ...nthesizedAnnotationAttributeProcessor.java | 62 + .../CombinationAnnotationElement.java | 165 + .../hutool/core/annotation/ForceAliasFor.java | 35 + .../ForceAliasedAnnotationAttribute.java | 49 + ...GenericSynthesizedAggregateAnnotation.java | 318 ++ .../GenericSynthesizedAnnotation.java | 197 + .../hutool/core/annotation/Hierarchical.java | 155 + .../java/cn/hutool/core/annotation/Link.java | 49 + .../cn/hutool/core/annotation/MirrorFor.java | 42 + .../MirrorLinkAnnotationPostProcessor.java | 132 + .../MirroredAnnotationAttribute.java | 48 + .../cn/hutool/core/annotation/PropIgnore.java | 21 + .../hutool/core/annotation/RelationType.java | 50 + .../SynthesizedAggregateAnnotation.java | 102 + .../annotation/SynthesizedAnnotation.java | 96 + ...nthesizedAnnotationAttributeProcessor.java | 24 + .../SynthesizedAnnotationPostProcessor.java | 71 + .../SynthesizedAnnotationProxy.java | 160 + .../SynthesizedAnnotationSelector.java | 82 + .../WrappedAnnotationAttribute.java | 125 + .../hutool/core/annotation/package-info.java | 7 + .../AbstractTypeAnnotationScanner.java | 288 ++ .../annotation/scanner/AnnotationScanner.java | 198 + .../scanner/ElementAnnotationScanner.java | 44 + .../scanner/EmptyAnnotationScanner.java | 31 + .../scanner/FieldAnnotationScanner.java | 47 + .../scanner/GenericAnnotationScanner.java | 149 + .../scanner/MetaAnnotationScanner.java | 110 + .../scanner/MethodAnnotationScanner.java | 133 + .../scanner/TypeAnnotationScanner.java | 105 + .../core/annotation/scanner/package-info.java | 7 + .../java/cn/hutool/core/bean/BeanDesc.java | 324 ++ .../cn/hutool/core/bean/BeanDescCache.java | 37 + .../cn/hutool/core/bean/BeanException.java | 32 + .../java/cn/hutool/core/bean/BeanPath.java | 324 ++ .../java/cn/hutool/core/bean/BeanUtil.java | 917 ++++ .../java/cn/hutool/core/bean/DynaBean.java | 226 + .../cn/hutool/core/bean/NullWrapperBean.java | 29 + .../java/cn/hutool/core/bean/PropDesc.java | 403 ++ .../cn/hutool/core/bean/copier/AbsCopier.java | 28 + .../hutool/core/bean/copier/BeanCopier.java | 94 + .../core/bean/copier/BeanToBeanCopier.java | 91 + .../core/bean/copier/BeanToMapCopier.java | 87 + .../hutool/core/bean/copier/CopyOptions.java | 383 ++ .../core/bean/copier/MapToBeanCopier.java | 119 + .../core/bean/copier/MapToMapCopier.java | 75 + .../core/bean/copier/ValueProvider.java | 34 + .../copier/ValueProviderToBeanCopier.java | 89 + .../hutool/core/bean/copier/package-info.java | 7 + .../copier/provider/BeanValueProvider.java | 100 + .../provider/DynaBeanValueProvider.java | 42 + .../bean/copier/provider/package-info.java | 7 + .../cn/hutool/core/bean/package-info.java | 7 + .../java/cn/hutool/core/builder/Builder.java | 19 + .../hutool/core/builder/CompareToBuilder.java | 975 ++++ .../cn/hutool/core/builder/EqualsBuilder.java | 563 ++ .../hutool/core/builder/GenericBuilder.java | 235 + .../hutool/core/builder/HashCodeBuilder.java | 958 ++++ .../java/cn/hutool/core/builder/IDKey.java | 61 + .../cn/hutool/core/builder/package-info.java | 8 + .../core/clone/CloneRuntimeException.java | 32 + .../cn/hutool/core/clone/CloneSupport.java | 21 + .../java/cn/hutool/core/clone/Cloneable.java | 16 + .../hutool/core/clone/DefaultCloneable.java | 28 + .../cn/hutool/core/clone/package-info.java | 7 + src/main/java/cn/hutool/core/codec/BCD.java | 129 + .../cn/hutool/core/codec/Base16Codec.java | 118 + .../java/cn/hutool/core/codec/Base32.java | 148 + .../cn/hutool/core/codec/Base32Codec.java | 215 + .../java/cn/hutool/core/codec/Base58.java | 152 + .../cn/hutool/core/codec/Base58Codec.java | 187 + .../java/cn/hutool/core/codec/Base62.java | 262 + .../cn/hutool/core/codec/Base62Codec.java | 232 + .../java/cn/hutool/core/codec/Base64.java | 386 ++ .../cn/hutool/core/codec/Base64Decoder.java | 162 + .../cn/hutool/core/codec/Base64Encoder.java | 214 + .../java/cn/hutool/core/codec/Caesar.java | 89 + .../java/cn/hutool/core/codec/Decoder.java | 20 + .../java/cn/hutool/core/codec/Encoder.java | 20 + .../java/cn/hutool/core/codec/Hashids.java | 506 ++ src/main/java/cn/hutool/core/codec/Morse.java | 172 + .../cn/hutool/core/codec/PercentCodec.java | 198 + .../java/cn/hutool/core/codec/PunyCode.java | 313 ++ src/main/java/cn/hutool/core/codec/Rot.java | 177 + .../cn/hutool/core/codec/package-info.java | 7 + .../cn/hutool/core/collection/ArrayIter.java | 133 + .../hutool/core/collection/AvgPartition.java | 59 + .../core/collection/BoundedPriorityQueue.java | 90 + .../core/collection/CollStreamUtil.java | 388 ++ .../cn/hutool/core/collection/CollUtil.java | 3103 +++++++++++ .../core/collection/CollectionUtil.java | 10 + .../hutool/core/collection/ComputeIter.java | 73 + .../core/collection/ConcurrentHashSet.java | 117 + .../cn/hutool/core/collection/CopiedIter.java | 68 + .../core/collection/EnumerationIter.java | 41 + .../cn/hutool/core/collection/FilterIter.java | 96 + .../cn/hutool/core/collection/IterChain.java | 91 + .../cn/hutool/core/collection/IterUtil.java | 1064 ++++ .../hutool/core/collection/IterableIter.java | 18 + .../core/collection/IteratorEnumeration.java | 37 + .../cn/hutool/core/collection/LineIter.java | 101 + .../cn/hutool/core/collection/ListUtil.java | 677 +++ .../hutool/core/collection/NodeListIter.java | 63 + .../cn/hutool/core/collection/Partition.java | 59 + .../hutool/core/collection/PartitionIter.java | 59 + .../collection/RandomAccessAvgPartition.java | 32 + .../collection/RandomAccessPartition.java | 27 + .../core/collection/ResettableIter.java | 18 + .../hutool/core/collection/RingIndexUtil.java | 80 + .../core/collection/SpliteratorUtil.java | 26 + .../core/collection/TransCollection.java | 73 + .../cn/hutool/core/collection/TransIter.java | 46 + .../core/collection/TransSpliterator.java | 51 + .../hutool/core/collection/UniqueKeySet.java | 177 + .../hutool/core/collection/package-info.java | 7 + .../core/comparator/BaseFieldComparator.java | 60 + .../core/comparator/ComparableComparator.java | 53 + .../core/comparator/ComparatorChain.java | 356 ++ .../core/comparator/ComparatorException.java | 32 + .../hutool/core/comparator/CompareUtil.java | 190 + .../core/comparator/FieldComparator.java | 65 + .../core/comparator/FieldsComparator.java | 50 + .../core/comparator/FuncComparator.java | 63 + .../core/comparator/IndexedComparator.java | 75 + .../core/comparator/InstanceComparator.java | 83 + .../core/comparator/LengthComparator.java | 25 + .../core/comparator/NullComparator.java | 78 + .../core/comparator/PinyinComparator.java | 31 + .../core/comparator/PropertyComparator.java | 34 + .../core/comparator/ReverseComparator.java | 49 + .../core/comparator/VersionComparator.java | 88 + .../hutool/core/comparator/package-info.java | 7 + .../java/cn/hutool/core/compress/Deflate.java | 102 + .../java/cn/hutool/core/compress/Gzip.java | 94 + .../hutool/core/compress/ZipCopyVisitor.java | 88 + .../cn/hutool/core/compress/ZipReader.java | 251 + .../cn/hutool/core/compress/ZipWriter.java | 287 ++ .../cn/hutool/core/compress/package-info.java | 7 + .../core/convert/AbstractConverter.java | 117 + .../cn/hutool/core/convert/BasicType.java | 60 + .../java/cn/hutool/core/convert/CastUtil.java | 118 + .../java/cn/hutool/core/convert/Convert.java | 1162 +++++ .../hutool/core/convert/ConvertException.java | 32 + .../cn/hutool/core/convert/Converter.java | 43 + .../core/convert/ConverterRegistry.java | 405 ++ .../core/convert/NumberChineseFormatter.java | 617 +++ .../hutool/core/convert/NumberWithFormat.java | 78 + .../core/convert/NumberWordFormatter.java | 182 + .../cn/hutool/core/convert/TypeConverter.java | 24 + .../core/convert/impl/ArrayConverter.java | 212 + .../convert/impl/AtomicBooleanConverter.java | 26 + .../impl/AtomicIntegerArrayConverter.java | 22 + .../impl/AtomicLongArrayConverter.java | 22 + .../impl/AtomicReferenceConverter.java | 36 + .../core/convert/impl/BeanConverter.java | 91 + .../core/convert/impl/BooleanConverter.java | 32 + .../core/convert/impl/CalendarConverter.java | 57 + .../core/convert/impl/CastConverter.java | 28 + .../core/convert/impl/CharacterConverter.java | 29 + .../core/convert/impl/CharsetConverter.java | 21 + .../core/convert/impl/ClassConverter.java | 39 + .../convert/impl/CollectionConverter.java | 78 + .../core/convert/impl/CurrencyConverter.java | 21 + .../core/convert/impl/DateConverter.java | 136 + .../core/convert/impl/DurationConverter.java | 29 + .../core/convert/impl/EnumConverter.java | 142 + .../core/convert/impl/LocaleConverter.java | 41 + .../core/convert/impl/MapConverter.java | 100 + .../core/convert/impl/NumberConverter.java | 261 + .../core/convert/impl/OptConverter.java | 21 + .../core/convert/impl/OptionalConverter.java | 22 + .../core/convert/impl/PathConverter.java | 41 + .../core/convert/impl/PeriodConverter.java | 29 + .../core/convert/impl/PrimitiveConverter.java | 92 + .../core/convert/impl/ReferenceConverter.java | 56 + .../impl/StackTraceElementConverter.java | 34 + .../core/convert/impl/StringConverter.java | 28 + .../impl/TemporalAccessorConverter.java | 308 ++ .../core/convert/impl/TimeZoneConverter.java | 20 + .../core/convert/impl/URIConverter.java | 34 + .../core/convert/impl/URLConverter.java | 34 + .../core/convert/impl/UUIDConverter.java | 22 + .../core/convert/impl/package-info.java | 7 + .../cn/hutool/core/convert/package-info.java | 7 + .../cn/hutool/core/date/BetweenFormatter.java | 208 + .../cn/hutool/core/date/CalendarUtil.java | 772 +++ .../java/cn/hutool/core/date/ChineseDate.java | 462 ++ .../java/cn/hutool/core/date/DateBetween.java | 185 + .../cn/hutool/core/date/DateException.java | 32 + .../java/cn/hutool/core/date/DateField.java | 158 + .../cn/hutool/core/date/DateModifier.java | 176 + .../java/cn/hutool/core/date/DatePattern.java | 322 ++ .../java/cn/hutool/core/date/DateRange.java | 59 + .../java/cn/hutool/core/date/DateTime.java | 1113 ++++ .../java/cn/hutool/core/date/DateUnit.java | 108 + .../java/cn/hutool/core/date/DateUtil.java | 2374 +++++++++ .../hutool/core/date/GroupTimeInterval.java | 177 + .../hutool/core/date/LocalDateTimeUtil.java | 639 +++ src/main/java/cn/hutool/core/date/Month.java | 246 + .../java/cn/hutool/core/date/Quarter.java | 61 + .../java/cn/hutool/core/date/StopWatch.java | 485 ++ .../java/cn/hutool/core/date/SystemClock.java | 70 + .../core/date/TemporalAccessorUtil.java | 228 + .../cn/hutool/core/date/TemporalUtil.java | 141 + .../cn/hutool/core/date/TimeInterval.java | 133 + src/main/java/cn/hutool/core/date/Week.java | 205 + src/main/java/cn/hutool/core/date/Zodiac.java | 103 + .../java/cn/hutool/core/date/ZoneUtil.java | 41 + .../core/date/chinese/ChineseMonth.java | 39 + .../cn/hutool/core/date/chinese/GanZhi.java | 81 + .../core/date/chinese/LunarFestival.java | 115 + .../hutool/core/date/chinese/LunarInfo.java | 116 + .../hutool/core/date/chinese/SolarTerms.java | 209 + .../core/date/chinese/package-info.java | 7 + .../core/date/format/AbstractDateBasic.java | 64 + .../cn/hutool/core/date/format/DateBasic.java | 34 + .../hutool/core/date/format/DateParser.java | 72 + .../hutool/core/date/format/DatePrinter.java | 78 + .../core/date/format/FastDateFormat.java | 420 ++ .../core/date/format/FastDateParser.java | 810 +++ .../core/date/format/FastDatePrinter.java | 1323 +++++ .../hutool/core/date/format/FormatCache.java | 175 + .../core/date/format/GlobalCustomFormat.java | 125 + .../hutool/core/date/format/package-info.java | 7 + .../cn/hutool/core/date/package-info.java | 7 + .../hutool/core/exceptions/CheckedUtil.java | 325 ++ .../core/exceptions/DependencyException.java | 37 + .../hutool/core/exceptions/ExceptionUtil.java | 428 ++ .../InvocationTargetRuntimeException.java | 34 + .../core/exceptions/NotInitedException.java | 36 + .../core/exceptions/StatefulException.java | 60 + .../hutool/core/exceptions/UtilException.java | 36 + .../core/exceptions/ValidateException.java | 47 + .../hutool/core/exceptions/package-info.java | 7 + .../hutool/core/getter/ArrayTypeGetter.java | 102 + .../hutool/core/getter/BasicTypeGetter.java | 131 + .../hutool/core/getter/GroupedTypeGetter.java | 103 + .../cn/hutool/core/getter/ListTypeGetter.java | 102 + .../core/getter/OptArrayTypeGetter.java | 117 + .../core/getter/OptBasicTypeGetter.java | 153 + .../OptNullBasicTypeFromObjectGetter.java | 133 + .../OptNullBasicTypeFromStringGetter.java | 80 + .../core/getter/OptNullBasicTypeGetter.java | 176 + .../cn/hutool/core/getter/package-info.java | 7 + .../cn/hutool/core/io/AppendableWriter.java | 106 + .../cn/hutool/core/io/BOMInputStream.java | 140 + .../java/cn/hutool/core/io/BomReader.java | 58 + .../java/cn/hutool/core/io/BufferUtil.java | 262 + .../cn/hutool/core/io/CharsetDetector.java | 114 + .../core/io/FastByteArrayOutputStream.java | 123 + .../cn/hutool/core/io/FastByteBuffer.java | 288 ++ .../cn/hutool/core/io/FastStringWriter.java | 90 + .../cn/hutool/core/io/FileMagicNumber.java | 1243 +++++ .../java/cn/hutool/core/io/FileTypeUtil.java | 244 + src/main/java/cn/hutool/core/io/FileUtil.java | 3593 +++++++++++++ .../cn/hutool/core/io/IORuntimeException.java | 44 + src/main/java/cn/hutool/core/io/IoUtil.java | 1332 +++++ .../cn/hutool/core/io/LimitedInputStream.java | 63 + .../java/cn/hutool/core/io/LineHandler.java | 15 + .../java/cn/hutool/core/io/ManifestUtil.java | 120 + src/main/java/cn/hutool/core/io/NioUtil.java | 275 + .../cn/hutool/core/io/NullOutputStream.java | 53 + .../cn/hutool/core/io/StreamProgress.java | 29 + .../core/io/ValidateObjectInputStream.java | 101 + .../cn/hutool/core/io/checksum/CRC16.java | 73 + .../java/cn/hutool/core/io/checksum/CRC8.java | 72 + .../core/io/checksum/crc16/CRC16Ansi.java | 33 + .../core/io/checksum/crc16/CRC16CCITT.java | 27 + .../io/checksum/crc16/CRC16CCITTFalse.java | 35 + .../core/io/checksum/crc16/CRC16Checksum.java | 75 + .../core/io/checksum/crc16/CRC16DNP.java | 33 + .../core/io/checksum/crc16/CRC16IBM.java | 27 + .../core/io/checksum/crc16/CRC16Maxim.java | 33 + .../core/io/checksum/crc16/CRC16Modbus.java | 33 + .../core/io/checksum/crc16/CRC16USB.java | 38 + .../core/io/checksum/crc16/CRC16X25.java | 38 + .../core/io/checksum/crc16/CRC16XModem.java | 32 + .../core/io/checksum/crc16/package-info.java | 7 + .../hutool/core/io/checksum/package-info.java | 7 + .../cn/hutool/core/io/copy/ChannelCopier.java | 116 + .../java/cn/hutool/core/io/copy/IoCopier.java | 76 + .../core/io/copy/ReaderWriterCopier.java | 117 + .../cn/hutool/core/io/copy/StreamCopier.java | 116 + .../cn/hutool/core/io/copy/package-info.java | 7 + .../cn/hutool/core/io/file/FileAppender.java | 127 + .../cn/hutool/core/io/file/FileCopier.java | 291 ++ .../java/cn/hutool/core/io/file/FileMode.java | 19 + .../cn/hutool/core/io/file/FileNameUtil.java | 285 + .../cn/hutool/core/io/file/FileReader.java | 306 ++ .../hutool/core/io/file/FileSystemUtil.java | 84 + .../cn/hutool/core/io/file/FileWrapper.java | 83 + .../cn/hutool/core/io/file/FileWriter.java | 424 ++ .../hutool/core/io/file/LineReadWatcher.java | 71 + .../cn/hutool/core/io/file/LineSeparator.java | 35 + .../cn/hutool/core/io/file/PathMover.java | 166 + .../java/cn/hutool/core/io/file/PathUtil.java | 686 +++ .../java/cn/hutool/core/io/file/Tailer.java | 236 + .../cn/hutool/core/io/file/package-info.java | 7 + .../core/io/file/visitor/CopyVisitor.java | 103 + .../core/io/file/visitor/DelVisitor.java | 44 + .../core/io/file/visitor/MoveVisitor.java | 75 + .../core/io/file/visitor/package-info.java | 7 + .../java/cn/hutool/core/io/package-info.java | 7 + .../core/io/resource/BytesResource.java | 70 + .../io/resource/CharSequenceResource.java | 91 + .../core/io/resource/ClassPathResource.java | 145 + .../hutool/core/io/resource/FileResource.java | 107 + .../core/io/resource/InputStreamResource.java | 54 + .../core/io/resource/MultiFileResource.java | 64 + .../core/io/resource/MultiResource.java | 132 + .../core/io/resource/NoResourceException.java | 47 + .../cn/hutool/core/io/resource/Resource.java | 125 + .../hutool/core/io/resource/ResourceUtil.java | 232 + .../core/io/resource/StringResource.java | 47 + .../hutool/core/io/resource/UrlResource.java | 107 + .../hutool/core/io/resource/VfsResource.java | 107 + .../core/io/resource/WebAppResource.java | 25 + .../hutool/core/io/resource/package-info.java | 7 + .../java/cn/hutool/core/io/unit/DataSize.java | 292 ++ .../cn/hutool/core/io/unit/DataSizeUtil.java | 38 + .../java/cn/hutool/core/io/unit/DataUnit.java | 80 + .../cn/hutool/core/io/unit/package-info.java | 7 + .../hutool/core/io/watch/SimpleWatcher.java | 13 + .../cn/hutool/core/io/watch/WatchAction.java | 24 + .../hutool/core/io/watch/WatchException.java | 33 + .../cn/hutool/core/io/watch/WatchKind.java | 67 + .../cn/hutool/core/io/watch/WatchMonitor.java | 426 ++ .../cn/hutool/core/io/watch/WatchServer.java | 189 + .../cn/hutool/core/io/watch/WatchUtil.java | 397 ++ .../java/cn/hutool/core/io/watch/Watcher.java | 44 + .../cn/hutool/core/io/watch/package-info.java | 7 + .../core/io/watch/watchers/DelayWatcher.java | 105 + .../core/io/watch/watchers/IgnoreWatcher.java | 32 + .../core/io/watch/watchers/WatcherChain.java | 81 + .../core/io/watch/watchers/package-info.java | 7 + src/main/java/cn/hutool/core/lang/Assert.java | 1121 ++++ src/main/java/cn/hutool/core/lang/Chain.java | 17 + .../cn/hutool/core/lang/ClassScanner.java | 460 ++ .../cn/hutool/core/lang/ConsistentHash.java | 101 + .../java/cn/hutool/core/lang/Console.java | 331 ++ .../cn/hutool/core/lang/ConsoleTable.java | 206 + .../cn/hutool/core/lang/DefaultSegment.java | 34 + src/main/java/cn/hutool/core/lang/Dict.java | 663 +++ src/main/java/cn/hutool/core/lang/Editor.java | 24 + .../java/cn/hutool/core/lang/EnumItem.java | 78 + src/main/java/cn/hutool/core/lang/Filter.java | 17 + .../cn/hutool/core/lang/JarClassLoader.java | 172 + .../java/cn/hutool/core/lang/Matcher.java | 18 + src/main/java/cn/hutool/core/lang/Opt.java | 568 ++ src/main/java/cn/hutool/core/lang/Pair.java | 87 + .../core/lang/ParameterizedTypeImpl.java | 102 + .../java/cn/hutool/core/lang/PatternPool.java | 275 + src/main/java/cn/hutool/core/lang/Range.java | 233 + .../java/cn/hutool/core/lang/RegexPool.java | 203 + .../java/cn/hutool/core/lang/Replacer.java | 22 + .../hutool/core/lang/ResourceClassLoader.java | 73 + .../java/cn/hutool/core/lang/Segment.java | 41 + .../java/cn/hutool/core/lang/SimpleCache.java | 192 + .../java/cn/hutool/core/lang/Singleton.java | 161 + src/main/java/cn/hutool/core/lang/Tuple.java | 179 + .../cn/hutool/core/lang/TypeReference.java | 51 + src/main/java/cn/hutool/core/lang/UUID.java | 447 ++ .../java/cn/hutool/core/lang/Validator.java | 1239 +++++ .../cn/hutool/core/lang/WeightRandom.java | 235 + .../hutool/core/lang/ansi/Ansi8BitColor.java | 93 + .../hutool/core/lang/ansi/AnsiBackground.java | 121 + .../cn/hutool/core/lang/ansi/AnsiColor.java | 121 + .../cn/hutool/core/lang/ansi/AnsiElement.java | 26 + .../cn/hutool/core/lang/ansi/AnsiEncoder.java | 65 + .../cn/hutool/core/lang/ansi/AnsiStyle.java | 61 + .../cn/hutool/core/lang/ansi/ForeOrBack.java | 16 + .../hutool/core/lang/ansi/package-info.java | 6 + .../cn/hutool/core/lang/caller/Caller.java | 47 + .../hutool/core/lang/caller/CallerUtil.java | 98 + .../lang/caller/SecurityManagerCaller.java | 56 + .../core/lang/caller/StackTraceCaller.java | 68 + .../hutool/core/lang/caller/package-info.java | 7 + .../cn/hutool/core/lang/copier/Copier.java | 16 + .../core/lang/copier/SrcToDestCopier.java | 86 + .../hutool/core/lang/copier/package-info.java | 7 + .../cn/hutool/core/lang/func/Consumer3.java | 23 + .../java/cn/hutool/core/lang/func/Func.java | 43 + .../java/cn/hutool/core/lang/func/Func0.java | 39 + .../java/cn/hutool/core/lang/func/Func1.java | 43 + .../cn/hutool/core/lang/func/LambdaUtil.java | 208 + .../cn/hutool/core/lang/func/Supplier1.java | 32 + .../cn/hutool/core/lang/func/Supplier2.java | 36 + .../cn/hutool/core/lang/func/Supplier3.java | 39 + .../cn/hutool/core/lang/func/Supplier4.java | 42 + .../cn/hutool/core/lang/func/Supplier5.java | 45 + .../cn/hutool/core/lang/func/VoidFunc.java | 41 + .../cn/hutool/core/lang/func/VoidFunc0.java | 37 + .../cn/hutool/core/lang/func/VoidFunc1.java | 39 + .../hutool/core/lang/func/package-info.java | 10 + .../hutool/core/lang/generator/Generator.java | 18 + .../core/lang/generator/ObjectGenerator.java | 28 + .../core/lang/generator/package-info.java | 7 + .../cn/hutool/core/lang/hash/CityHash.java | 499 ++ .../java/cn/hutool/core/lang/hash/Hash.java | 19 + .../cn/hutool/core/lang/hash/Hash128.java | 25 + .../java/cn/hutool/core/lang/hash/Hash32.java | 24 + .../java/cn/hutool/core/lang/hash/Hash64.java | 24 + .../cn/hutool/core/lang/hash/KetamaHash.java | 51 + .../cn/hutool/core/lang/hash/MetroHash.java | 217 + .../cn/hutool/core/lang/hash/MurmurHash.java | 363 ++ .../cn/hutool/core/lang/hash/Number128.java | 90 + .../hutool/core/lang/hash/package-info.java | 7 + .../java/cn/hutool/core/lang/id/NanoId.java | 103 + .../cn/hutool/core/lang/id/package-info.java | 7 + .../hutool/core/lang/intern/InternUtil.java | 40 + .../cn/hutool/core/lang/intern/Interner.java | 21 + .../core/lang/intern/JdkStringInterner.java | 17 + .../hutool/core/lang/intern/WeakInterner.java | 27 + .../hutool/core/lang/intern/package-info.java | 8 + .../hutool/core/lang/loader/AtomicLoader.java | 51 + .../core/lang/loader/LazyFunLoader.java | 78 + .../hutool/core/lang/loader/LazyLoader.java | 45 + .../cn/hutool/core/lang/loader/Loader.java | 21 + .../hutool/core/lang/loader/package-info.java | 7 + .../cn/hutool/core/lang/mutable/Mutable.java | 23 + .../hutool/core/lang/mutable/MutableBool.java | 100 + .../hutool/core/lang/mutable/MutableByte.java | 198 + .../core/lang/mutable/MutableDouble.java | 192 + .../core/lang/mutable/MutableFloat.java | 193 + .../hutool/core/lang/mutable/MutableInt.java | 193 + .../hutool/core/lang/mutable/MutableLong.java | 205 + .../hutool/core/lang/mutable/MutableObj.java | 82 + .../hutool/core/lang/mutable/MutablePair.java | 57 + .../core/lang/mutable/MutableShort.java | 198 + .../core/lang/mutable/package-info.java | 7 + .../cn/hutool/core/lang/package-info.java | 7 + .../lang/reflect/ActualTypeMapperPool.java | 118 + .../core/lang/reflect/LookupFactory.java | 76 + .../core/lang/reflect/MethodHandleUtil.java | 227 + .../core/lang/reflect/package-info.java | 7 + .../java/cn/hutool/core/lang/tree/Node.java | 86 + .../java/cn/hutool/core/lang/tree/Tree.java | 355 ++ .../cn/hutool/core/lang/tree/TreeBuilder.java | 307 ++ .../cn/hutool/core/lang/tree/TreeNode.java | 150 + .../hutool/core/lang/tree/TreeNodeConfig.java | 148 + .../cn/hutool/core/lang/tree/TreeUtil.java | 239 + .../hutool/core/lang/tree/package-info.java | 14 + .../lang/tree/parser/DefaultNodeParser.java | 30 + .../core/lang/tree/parser/NodeParser.java | 19 + .../java/cn/hutool/core/map/AbsEntry.java | 46 + src/main/java/cn/hutool/core/map/BiMap.java | 133 + .../hutool/core/map/CamelCaseLinkedMap.java | 66 + .../java/cn/hutool/core/map/CamelCaseMap.java | 87 + .../core/map/CaseInsensitiveLinkedMap.java | 67 + .../hutool/core/map/CaseInsensitiveMap.java | 87 + .../core/map/CaseInsensitiveTreeMap.java | 59 + .../java/cn/hutool/core/map/CustomKeyMap.java | 32 + .../hutool/core/map/FixedLinkedHashMap.java | 77 + .../java/cn/hutool/core/map/ForestMap.java | 333 ++ .../java/cn/hutool/core/map/FuncKeyMap.java | 48 + src/main/java/cn/hutool/core/map/FuncMap.java | 73 + .../cn/hutool/core/map/LinkedForestMap.java | 736 +++ .../java/cn/hutool/core/map/MapBuilder.java | 188 + .../java/cn/hutool/core/map/MapProxy.java | 183 + src/main/java/cn/hutool/core/map/MapUtil.java | 1488 ++++++ .../java/cn/hutool/core/map/MapWrapper.java | 236 + .../core/map/ReferenceConcurrentMap.java | 322 ++ .../core/map/SafeConcurrentHashMap.java | 74 + .../java/cn/hutool/core/map/TableMap.java | 330 ++ .../java/cn/hutool/core/map/TolerantMap.java | 103 + .../java/cn/hutool/core/map/TransMap.java | 129 + .../java/cn/hutool/core/map/TreeEntry.java | 143 + .../cn/hutool/core/map/WeakConcurrentMap.java | 34 + .../core/map/multi/AbsCollValueMap.java | 151 + .../cn/hutool/core/map/multi/AbsTable.java | 236 + .../core/map/multi/CollectionValueMap.java | 102 + .../hutool/core/map/multi/ListValueMap.java | 73 + .../cn/hutool/core/map/multi/RowKeyTable.java | 281 + .../cn/hutool/core/map/multi/SetValueMap.java | 73 + .../java/cn/hutool/core/map/multi/Table.java | 281 + .../hutool/core/map/multi/package-info.java | 7 + .../java/cn/hutool/core/map/package-info.java | 7 + .../java/cn/hutool/core/math/Arrangement.java | 127 + .../cn/hutool/core/math/BitStatusUtil.java | 81 + .../java/cn/hutool/core/math/Calculator.java | 201 + .../java/cn/hutool/core/math/Combination.java | 107 + .../java/cn/hutool/core/math/MathUtil.java | 103 + src/main/java/cn/hutool/core/math/Money.java | 854 +++ .../cn/hutool/core/math/package-info.java | 7 + .../hutool/core/net/DefaultTrustManager.java | 51 + .../cn/hutool/core/net/FormUrlencoded.java | 19 + .../java/cn/hutool/core/net/Ipv4Util.java | 410 ++ src/main/java/cn/hutool/core/net/MaskBit.java | 76 + src/main/java/cn/hutool/core/net/NetUtil.java | 833 +++ .../java/cn/hutool/core/net/PassAuth.java | 41 + src/main/java/cn/hutool/core/net/RFC3986.java | 104 + .../cn/hutool/core/net/SSLContextBuilder.java | 137 + .../java/cn/hutool/core/net/SSLProtocols.java | 40 + src/main/java/cn/hutool/core/net/SSLUtil.java | 60 + .../java/cn/hutool/core/net/URLDecoder.java | 138 + .../cn/hutool/core/net/URLEncodeUtil.java | 187 + .../java/cn/hutool/core/net/URLEncoder.java | 407 ++ .../core/net/UserPassAuthenticator.java | 33 + .../core/net/multipart/MultipartFormData.java | 273 + .../MultipartRequestInputStream.java | 242 + .../hutool/core/net/multipart/UploadFile.java | 272 + .../core/net/multipart/UploadFileHeader.java | 204 + .../core/net/multipart/UploadSetting.java | 110 + .../core/net/multipart/package-info.java | 7 + .../java/cn/hutool/core/net/package-info.java | 7 + .../cn/hutool/core/net/url/UrlBuilder.java | 583 +++ .../java/cn/hutool/core/net/url/UrlPath.java | 219 + .../java/cn/hutool/core/net/url/UrlQuery.java | 415 ++ .../cn/hutool/core/net/url/package-info.java | 7 + .../java/cn/hutool/core/package-info.java | 7 + .../cn/hutool/core/stream/CollectorUtil.java | 314 ++ .../hutool/core/stream/SimpleCollector.java | 109 + .../cn/hutool/core/stream/StreamUtil.java | 170 + .../cn/hutool/core/stream/package-info.java | 7 + .../cn/hutool/core/text/ASCIIStrCache.java | 30 + .../cn/hutool/core/text/AntPathMatcher.java | 945 ++++ .../java/cn/hutool/core/text/CharPool.java | 86 + .../cn/hutool/core/text/CharSequenceUtil.java | 4589 +++++++++++++++++ .../java/cn/hutool/core/text/NamingCase.java | 192 + .../cn/hutool/core/text/PasswdStrength.java | 288 ++ .../java/cn/hutool/core/text/Simhash.java | 195 + .../java/cn/hutool/core/text/StrBuilder.java | 587 +++ .../cn/hutool/core/text/StrFormatter.java | 126 + .../java/cn/hutool/core/text/StrJoiner.java | 434 ++ .../java/cn/hutool/core/text/StrMatcher.java | 119 + .../java/cn/hutool/core/text/StrPool.java | 180 + .../java/cn/hutool/core/text/StrSplitter.java | 457 ++ .../cn/hutool/core/text/TextSimilarity.java | 169 + .../java/cn/hutool/core/text/UnicodeUtil.java | 116 + .../hutool/core/text/csv/CsvBaseReader.java | 280 + .../cn/hutool/core/text/csv/CsvConfig.java | 119 + .../java/cn/hutool/core/text/csv/CsvData.java | 83 + .../cn/hutool/core/text/csv/CsvParser.java | 447 ++ .../hutool/core/text/csv/CsvReadConfig.java | 118 + .../cn/hutool/core/text/csv/CsvReader.java | 153 + .../java/cn/hutool/core/text/csv/CsvRow.java | 267 + .../hutool/core/text/csv/CsvRowHandler.java | 18 + .../java/cn/hutool/core/text/csv/CsvUtil.java | 141 + .../hutool/core/text/csv/CsvWriteConfig.java | 54 + .../cn/hutool/core/text/csv/CsvWriter.java | 451 ++ .../cn/hutool/core/text/csv/package-info.java | 8 + .../hutool/core/text/escape/Html4Escape.java | 316 ++ .../core/text/escape/Html4Unescape.java | 22 + .../core/text/escape/InternalEscapeUtil.java | 24 + .../text/escape/NumericEntityUnescaper.java | 56 + .../cn/hutool/core/text/escape/XmlEscape.java | 38 + .../hutool/core/text/escape/XmlUnescape.java | 27 + .../hutool/core/text/escape/package-info.java | 7 + .../hutool/core/text/finder/CharFinder.java | 66 + .../core/text/finder/CharMatcherFinder.java | 53 + .../cn/hutool/core/text/finder/Finder.java | 36 + .../hutool/core/text/finder/LengthFinder.java | 49 + .../core/text/finder/PatternFinder.java | 77 + .../cn/hutool/core/text/finder/StrFinder.java | 64 + .../hutool/core/text/finder/TextFinder.java | 74 + .../hutool/core/text/finder/package-info.java | 13 + .../cn/hutool/core/text/package-info.java | 7 + .../core/text/replacer/LookupReplacer.java | 74 + .../core/text/replacer/ReplacerChain.java | 56 + .../core/text/replacer/StrReplacer.java | 45 + .../core/text/replacer/package-info.java | 7 + .../cn/hutool/core/text/split/SplitIter.java | 153 + .../java/cn/hutool/core/thread/AsyncUtil.java | 63 + .../cn/hutool/core/thread/BlockPolicy.java | 56 + .../hutool/core/thread/ConcurrencyTester.java | 89 + .../core/thread/DelegatedExecutorService.java | 100 + .../hutool/core/thread/ExecutorBuilder.java | 260 + .../FinalizableDelegatedExecutorService.java | 25 + .../hutool/core/thread/GlobalThreadPool.java | 96 + .../core/thread/NamedThreadFactory.java | 98 + .../cn/hutool/core/thread/RejectPolicy.java | 42 + .../hutool/core/thread/SemaphoreRunnable.java | 59 + .../cn/hutool/core/thread/SyncFinisher.java | 232 + .../hutool/core/thread/ThreadException.java | 38 + .../core/thread/ThreadFactoryBuilder.java | 157 + .../cn/hutool/core/thread/ThreadUtil.java | 676 +++ .../cn/hutool/core/thread/lock/LockUtil.java | 43 + .../cn/hutool/core/thread/lock/NoLock.java | 46 + .../core/thread/lock/NoReadWriteLock.java | 22 + .../hutool/core/thread/lock/package-info.java | 7 + .../cn/hutool/core/thread/package-info.java | 7 + .../NamedInheritableThreadLocal.java | 28 + .../thread/threadlocal/NamedThreadLocal.java | 28 + .../core/thread/threadlocal/package-info.java | 7 + .../java/cn/hutool/core/util/ArrayUtil.java | 1975 +++++++ .../java/cn/hutool/core/util/BooleanUtil.java | 504 ++ .../java/cn/hutool/core/util/ByteUtil.java | 489 ++ .../java/cn/hutool/core/util/CharUtil.java | 391 ++ .../java/cn/hutool/core/util/CharsetUtil.java | 226 + .../cn/hutool/core/util/ClassLoaderUtil.java | 344 ++ .../java/cn/hutool/core/util/ClassUtil.java | 1135 ++++ .../cn/hutool/core/util/CoordinateUtil.java | 330 ++ .../cn/hutool/core/util/CreditCodeUtil.java | 138 + .../cn/hutool/core/util/DesensitizedUtil.java | 360 ++ .../java/cn/hutool/core/util/EnumUtil.java | 371 ++ .../java/cn/hutool/core/util/EscapeUtil.java | 199 + .../java/cn/hutool/core/util/HashUtil.java | 629 +++ .../java/cn/hutool/core/util/HexUtil.java | 326 ++ .../java/cn/hutool/core/util/IdcardUtil.java | 809 +++ .../cn/hutool/core/util/ModifierUtil.java | 335 ++ .../java/cn/hutool/core/util/NumberUtil.java | 2801 ++++++++++ .../java/cn/hutool/core/util/ObjUtil.java | 9 + .../java/cn/hutool/core/util/ObjectUtil.java | 753 +++ .../java/cn/hutool/core/util/PageUtil.java | 274 + .../java/cn/hutool/core/util/PhoneUtil.java | 188 + .../hutool/core/util/PrimitiveArrayUtil.java | 3217 ++++++++++++ .../java/cn/hutool/core/util/RadixUtil.java | 126 + .../java/cn/hutool/core/util/RandomUtil.java | 651 +++ src/main/java/cn/hutool/core/util/ReUtil.java | 989 ++++ .../cn/hutool/core/util/ReferenceUtil.java | 75 + .../java/cn/hutool/core/util/ReflectUtil.java | 1194 +++++ .../cn/hutool/core/util/SerializeUtil.java | 67 + .../hutool/core/util/ServiceLoaderUtil.java | 106 + .../java/cn/hutool/core/util/StrUtil.java | 461 ++ .../cn/hutool/core/util/SystemPropsUtil.java | 141 + .../java/cn/hutool/core/util/TypeUtil.java | 403 ++ .../java/cn/hutool/core/util/URLUtil.java | 779 +++ .../java/cn/hutool/core/util/ZipUtil.java | 1041 ++++ .../cn/hutool/core/util/package-info.java | 7 + .../java/com/keyware/regtool/AESUtil.java | 220 + .../java/com/keyware/regtool/Application.java | 49 + src/main/java/com/keyware/regtool/Main.java | 13 + .../java/com/keyware/regtool/RegInfo.java | 49 + .../keyware/regtool/WebViewController.java | 72 + src/main/java/module-info.java | 10 + src/main/resources/META-INF/MANIFEST.MF | 3 + src/main/resources/logo.ico | Bin 0 -> 292878 bytes src/main/resources/logo.png | Bin 0 -> 4064 bytes .../resources/scripts/layui/css/layui.css | 1 + .../resources/scripts/layui/font/iconfont.eot | Bin 0 -> 54172 bytes .../resources/scripts/layui/font/iconfont.svg | 405 ++ .../resources/scripts/layui/font/iconfont.ttf | Bin 0 -> 53996 bytes .../scripts/layui/font/iconfont.woff | Bin 0 -> 34624 bytes .../scripts/layui/font/iconfont.woff2 | Bin 0 -> 29736 bytes src/main/resources/scripts/layui/layui.js | 1 + src/main/resources/style/index.css | 9 + src/main/resources/template/index.html | 80 + 659 files changed, 123404 insertions(+) create mode 100644 .gitignore create mode 100644 .mvn/wrapper/maven-wrapper.jar create mode 100644 .mvn/wrapper/maven-wrapper.properties create mode 100644 mvnw create mode 100644 mvnw.cmd create mode 100644 package.bat create mode 100644 pom.xml create mode 100644 src/main/java/cn/hutool/core/annotation/AbstractAnnotationSynthesizer.java create mode 100644 src/main/java/cn/hutool/core/annotation/AbstractLinkAnnotationPostProcessor.java create mode 100644 src/main/java/cn/hutool/core/annotation/AbstractWrappedAnnotationAttribute.java create mode 100644 src/main/java/cn/hutool/core/annotation/AggregateAnnotation.java create mode 100644 src/main/java/cn/hutool/core/annotation/Alias.java create mode 100644 src/main/java/cn/hutool/core/annotation/AliasAnnotationPostProcessor.java create mode 100644 src/main/java/cn/hutool/core/annotation/AliasFor.java create mode 100644 src/main/java/cn/hutool/core/annotation/AliasLinkAnnotationPostProcessor.java create mode 100644 src/main/java/cn/hutool/core/annotation/AliasedAnnotationAttribute.java create mode 100644 src/main/java/cn/hutool/core/annotation/AnnotationAttribute.java create mode 100644 src/main/java/cn/hutool/core/annotation/AnnotationAttributeValueProvider.java create mode 100644 src/main/java/cn/hutool/core/annotation/AnnotationProxy.java create mode 100644 src/main/java/cn/hutool/core/annotation/AnnotationSynthesizer.java create mode 100644 src/main/java/cn/hutool/core/annotation/AnnotationUtil.java create mode 100644 src/main/java/cn/hutool/core/annotation/CacheableAnnotationAttribute.java create mode 100644 src/main/java/cn/hutool/core/annotation/CacheableSynthesizedAnnotationAttributeProcessor.java create mode 100644 src/main/java/cn/hutool/core/annotation/CombinationAnnotationElement.java create mode 100644 src/main/java/cn/hutool/core/annotation/ForceAliasFor.java create mode 100644 src/main/java/cn/hutool/core/annotation/ForceAliasedAnnotationAttribute.java create mode 100644 src/main/java/cn/hutool/core/annotation/GenericSynthesizedAggregateAnnotation.java create mode 100644 src/main/java/cn/hutool/core/annotation/GenericSynthesizedAnnotation.java create mode 100644 src/main/java/cn/hutool/core/annotation/Hierarchical.java create mode 100644 src/main/java/cn/hutool/core/annotation/Link.java create mode 100644 src/main/java/cn/hutool/core/annotation/MirrorFor.java create mode 100644 src/main/java/cn/hutool/core/annotation/MirrorLinkAnnotationPostProcessor.java create mode 100644 src/main/java/cn/hutool/core/annotation/MirroredAnnotationAttribute.java create mode 100644 src/main/java/cn/hutool/core/annotation/PropIgnore.java create mode 100644 src/main/java/cn/hutool/core/annotation/RelationType.java create mode 100644 src/main/java/cn/hutool/core/annotation/SynthesizedAggregateAnnotation.java create mode 100644 src/main/java/cn/hutool/core/annotation/SynthesizedAnnotation.java create mode 100644 src/main/java/cn/hutool/core/annotation/SynthesizedAnnotationAttributeProcessor.java create mode 100644 src/main/java/cn/hutool/core/annotation/SynthesizedAnnotationPostProcessor.java create mode 100644 src/main/java/cn/hutool/core/annotation/SynthesizedAnnotationProxy.java create mode 100644 src/main/java/cn/hutool/core/annotation/SynthesizedAnnotationSelector.java create mode 100644 src/main/java/cn/hutool/core/annotation/WrappedAnnotationAttribute.java create mode 100644 src/main/java/cn/hutool/core/annotation/package-info.java create mode 100644 src/main/java/cn/hutool/core/annotation/scanner/AbstractTypeAnnotationScanner.java create mode 100644 src/main/java/cn/hutool/core/annotation/scanner/AnnotationScanner.java create mode 100644 src/main/java/cn/hutool/core/annotation/scanner/ElementAnnotationScanner.java create mode 100644 src/main/java/cn/hutool/core/annotation/scanner/EmptyAnnotationScanner.java create mode 100644 src/main/java/cn/hutool/core/annotation/scanner/FieldAnnotationScanner.java create mode 100644 src/main/java/cn/hutool/core/annotation/scanner/GenericAnnotationScanner.java create mode 100644 src/main/java/cn/hutool/core/annotation/scanner/MetaAnnotationScanner.java create mode 100644 src/main/java/cn/hutool/core/annotation/scanner/MethodAnnotationScanner.java create mode 100644 src/main/java/cn/hutool/core/annotation/scanner/TypeAnnotationScanner.java create mode 100644 src/main/java/cn/hutool/core/annotation/scanner/package-info.java create mode 100644 src/main/java/cn/hutool/core/bean/BeanDesc.java create mode 100644 src/main/java/cn/hutool/core/bean/BeanDescCache.java create mode 100644 src/main/java/cn/hutool/core/bean/BeanException.java create mode 100644 src/main/java/cn/hutool/core/bean/BeanPath.java create mode 100644 src/main/java/cn/hutool/core/bean/BeanUtil.java create mode 100644 src/main/java/cn/hutool/core/bean/DynaBean.java create mode 100644 src/main/java/cn/hutool/core/bean/NullWrapperBean.java create mode 100644 src/main/java/cn/hutool/core/bean/PropDesc.java create mode 100644 src/main/java/cn/hutool/core/bean/copier/AbsCopier.java create mode 100644 src/main/java/cn/hutool/core/bean/copier/BeanCopier.java create mode 100644 src/main/java/cn/hutool/core/bean/copier/BeanToBeanCopier.java create mode 100644 src/main/java/cn/hutool/core/bean/copier/BeanToMapCopier.java create mode 100644 src/main/java/cn/hutool/core/bean/copier/CopyOptions.java create mode 100644 src/main/java/cn/hutool/core/bean/copier/MapToBeanCopier.java create mode 100644 src/main/java/cn/hutool/core/bean/copier/MapToMapCopier.java create mode 100644 src/main/java/cn/hutool/core/bean/copier/ValueProvider.java create mode 100644 src/main/java/cn/hutool/core/bean/copier/ValueProviderToBeanCopier.java create mode 100644 src/main/java/cn/hutool/core/bean/copier/package-info.java create mode 100644 src/main/java/cn/hutool/core/bean/copier/provider/BeanValueProvider.java create mode 100644 src/main/java/cn/hutool/core/bean/copier/provider/DynaBeanValueProvider.java create mode 100644 src/main/java/cn/hutool/core/bean/copier/provider/package-info.java create mode 100644 src/main/java/cn/hutool/core/bean/package-info.java create mode 100644 src/main/java/cn/hutool/core/builder/Builder.java create mode 100644 src/main/java/cn/hutool/core/builder/CompareToBuilder.java create mode 100644 src/main/java/cn/hutool/core/builder/EqualsBuilder.java create mode 100644 src/main/java/cn/hutool/core/builder/GenericBuilder.java create mode 100644 src/main/java/cn/hutool/core/builder/HashCodeBuilder.java create mode 100644 src/main/java/cn/hutool/core/builder/IDKey.java create mode 100644 src/main/java/cn/hutool/core/builder/package-info.java create mode 100644 src/main/java/cn/hutool/core/clone/CloneRuntimeException.java create mode 100644 src/main/java/cn/hutool/core/clone/CloneSupport.java create mode 100644 src/main/java/cn/hutool/core/clone/Cloneable.java create mode 100644 src/main/java/cn/hutool/core/clone/DefaultCloneable.java create mode 100644 src/main/java/cn/hutool/core/clone/package-info.java create mode 100644 src/main/java/cn/hutool/core/codec/BCD.java create mode 100644 src/main/java/cn/hutool/core/codec/Base16Codec.java create mode 100644 src/main/java/cn/hutool/core/codec/Base32.java create mode 100644 src/main/java/cn/hutool/core/codec/Base32Codec.java create mode 100644 src/main/java/cn/hutool/core/codec/Base58.java create mode 100644 src/main/java/cn/hutool/core/codec/Base58Codec.java create mode 100644 src/main/java/cn/hutool/core/codec/Base62.java create mode 100644 src/main/java/cn/hutool/core/codec/Base62Codec.java create mode 100644 src/main/java/cn/hutool/core/codec/Base64.java create mode 100644 src/main/java/cn/hutool/core/codec/Base64Decoder.java create mode 100644 src/main/java/cn/hutool/core/codec/Base64Encoder.java create mode 100644 src/main/java/cn/hutool/core/codec/Caesar.java create mode 100644 src/main/java/cn/hutool/core/codec/Decoder.java create mode 100644 src/main/java/cn/hutool/core/codec/Encoder.java create mode 100644 src/main/java/cn/hutool/core/codec/Hashids.java create mode 100644 src/main/java/cn/hutool/core/codec/Morse.java create mode 100644 src/main/java/cn/hutool/core/codec/PercentCodec.java create mode 100644 src/main/java/cn/hutool/core/codec/PunyCode.java create mode 100644 src/main/java/cn/hutool/core/codec/Rot.java create mode 100644 src/main/java/cn/hutool/core/codec/package-info.java create mode 100644 src/main/java/cn/hutool/core/collection/ArrayIter.java create mode 100644 src/main/java/cn/hutool/core/collection/AvgPartition.java create mode 100644 src/main/java/cn/hutool/core/collection/BoundedPriorityQueue.java create mode 100644 src/main/java/cn/hutool/core/collection/CollStreamUtil.java create mode 100644 src/main/java/cn/hutool/core/collection/CollUtil.java create mode 100644 src/main/java/cn/hutool/core/collection/CollectionUtil.java create mode 100644 src/main/java/cn/hutool/core/collection/ComputeIter.java create mode 100644 src/main/java/cn/hutool/core/collection/ConcurrentHashSet.java create mode 100644 src/main/java/cn/hutool/core/collection/CopiedIter.java create mode 100644 src/main/java/cn/hutool/core/collection/EnumerationIter.java create mode 100644 src/main/java/cn/hutool/core/collection/FilterIter.java create mode 100644 src/main/java/cn/hutool/core/collection/IterChain.java create mode 100644 src/main/java/cn/hutool/core/collection/IterUtil.java create mode 100644 src/main/java/cn/hutool/core/collection/IterableIter.java create mode 100644 src/main/java/cn/hutool/core/collection/IteratorEnumeration.java create mode 100644 src/main/java/cn/hutool/core/collection/LineIter.java create mode 100644 src/main/java/cn/hutool/core/collection/ListUtil.java create mode 100644 src/main/java/cn/hutool/core/collection/NodeListIter.java create mode 100644 src/main/java/cn/hutool/core/collection/Partition.java create mode 100644 src/main/java/cn/hutool/core/collection/PartitionIter.java create mode 100644 src/main/java/cn/hutool/core/collection/RandomAccessAvgPartition.java create mode 100644 src/main/java/cn/hutool/core/collection/RandomAccessPartition.java create mode 100644 src/main/java/cn/hutool/core/collection/ResettableIter.java create mode 100644 src/main/java/cn/hutool/core/collection/RingIndexUtil.java create mode 100644 src/main/java/cn/hutool/core/collection/SpliteratorUtil.java create mode 100644 src/main/java/cn/hutool/core/collection/TransCollection.java create mode 100644 src/main/java/cn/hutool/core/collection/TransIter.java create mode 100644 src/main/java/cn/hutool/core/collection/TransSpliterator.java create mode 100644 src/main/java/cn/hutool/core/collection/UniqueKeySet.java create mode 100644 src/main/java/cn/hutool/core/collection/package-info.java create mode 100644 src/main/java/cn/hutool/core/comparator/BaseFieldComparator.java create mode 100644 src/main/java/cn/hutool/core/comparator/ComparableComparator.java create mode 100644 src/main/java/cn/hutool/core/comparator/ComparatorChain.java create mode 100644 src/main/java/cn/hutool/core/comparator/ComparatorException.java create mode 100644 src/main/java/cn/hutool/core/comparator/CompareUtil.java create mode 100644 src/main/java/cn/hutool/core/comparator/FieldComparator.java create mode 100644 src/main/java/cn/hutool/core/comparator/FieldsComparator.java create mode 100644 src/main/java/cn/hutool/core/comparator/FuncComparator.java create mode 100644 src/main/java/cn/hutool/core/comparator/IndexedComparator.java create mode 100644 src/main/java/cn/hutool/core/comparator/InstanceComparator.java create mode 100644 src/main/java/cn/hutool/core/comparator/LengthComparator.java create mode 100644 src/main/java/cn/hutool/core/comparator/NullComparator.java create mode 100644 src/main/java/cn/hutool/core/comparator/PinyinComparator.java create mode 100644 src/main/java/cn/hutool/core/comparator/PropertyComparator.java create mode 100644 src/main/java/cn/hutool/core/comparator/ReverseComparator.java create mode 100644 src/main/java/cn/hutool/core/comparator/VersionComparator.java create mode 100644 src/main/java/cn/hutool/core/comparator/package-info.java create mode 100644 src/main/java/cn/hutool/core/compress/Deflate.java create mode 100644 src/main/java/cn/hutool/core/compress/Gzip.java create mode 100644 src/main/java/cn/hutool/core/compress/ZipCopyVisitor.java create mode 100644 src/main/java/cn/hutool/core/compress/ZipReader.java create mode 100644 src/main/java/cn/hutool/core/compress/ZipWriter.java create mode 100644 src/main/java/cn/hutool/core/compress/package-info.java create mode 100644 src/main/java/cn/hutool/core/convert/AbstractConverter.java create mode 100644 src/main/java/cn/hutool/core/convert/BasicType.java create mode 100644 src/main/java/cn/hutool/core/convert/CastUtil.java create mode 100644 src/main/java/cn/hutool/core/convert/Convert.java create mode 100644 src/main/java/cn/hutool/core/convert/ConvertException.java create mode 100644 src/main/java/cn/hutool/core/convert/Converter.java create mode 100644 src/main/java/cn/hutool/core/convert/ConverterRegistry.java create mode 100644 src/main/java/cn/hutool/core/convert/NumberChineseFormatter.java create mode 100644 src/main/java/cn/hutool/core/convert/NumberWithFormat.java create mode 100644 src/main/java/cn/hutool/core/convert/NumberWordFormatter.java create mode 100644 src/main/java/cn/hutool/core/convert/TypeConverter.java create mode 100644 src/main/java/cn/hutool/core/convert/impl/ArrayConverter.java create mode 100644 src/main/java/cn/hutool/core/convert/impl/AtomicBooleanConverter.java create mode 100644 src/main/java/cn/hutool/core/convert/impl/AtomicIntegerArrayConverter.java create mode 100644 src/main/java/cn/hutool/core/convert/impl/AtomicLongArrayConverter.java create mode 100644 src/main/java/cn/hutool/core/convert/impl/AtomicReferenceConverter.java create mode 100644 src/main/java/cn/hutool/core/convert/impl/BeanConverter.java create mode 100644 src/main/java/cn/hutool/core/convert/impl/BooleanConverter.java create mode 100644 src/main/java/cn/hutool/core/convert/impl/CalendarConverter.java create mode 100644 src/main/java/cn/hutool/core/convert/impl/CastConverter.java create mode 100644 src/main/java/cn/hutool/core/convert/impl/CharacterConverter.java create mode 100644 src/main/java/cn/hutool/core/convert/impl/CharsetConverter.java create mode 100644 src/main/java/cn/hutool/core/convert/impl/ClassConverter.java create mode 100644 src/main/java/cn/hutool/core/convert/impl/CollectionConverter.java create mode 100644 src/main/java/cn/hutool/core/convert/impl/CurrencyConverter.java create mode 100644 src/main/java/cn/hutool/core/convert/impl/DateConverter.java create mode 100644 src/main/java/cn/hutool/core/convert/impl/DurationConverter.java create mode 100644 src/main/java/cn/hutool/core/convert/impl/EnumConverter.java create mode 100644 src/main/java/cn/hutool/core/convert/impl/LocaleConverter.java create mode 100644 src/main/java/cn/hutool/core/convert/impl/MapConverter.java create mode 100644 src/main/java/cn/hutool/core/convert/impl/NumberConverter.java create mode 100644 src/main/java/cn/hutool/core/convert/impl/OptConverter.java create mode 100644 src/main/java/cn/hutool/core/convert/impl/OptionalConverter.java create mode 100644 src/main/java/cn/hutool/core/convert/impl/PathConverter.java create mode 100644 src/main/java/cn/hutool/core/convert/impl/PeriodConverter.java create mode 100644 src/main/java/cn/hutool/core/convert/impl/PrimitiveConverter.java create mode 100644 src/main/java/cn/hutool/core/convert/impl/ReferenceConverter.java create mode 100644 src/main/java/cn/hutool/core/convert/impl/StackTraceElementConverter.java create mode 100644 src/main/java/cn/hutool/core/convert/impl/StringConverter.java create mode 100644 src/main/java/cn/hutool/core/convert/impl/TemporalAccessorConverter.java create mode 100644 src/main/java/cn/hutool/core/convert/impl/TimeZoneConverter.java create mode 100644 src/main/java/cn/hutool/core/convert/impl/URIConverter.java create mode 100644 src/main/java/cn/hutool/core/convert/impl/URLConverter.java create mode 100644 src/main/java/cn/hutool/core/convert/impl/UUIDConverter.java create mode 100644 src/main/java/cn/hutool/core/convert/impl/package-info.java create mode 100644 src/main/java/cn/hutool/core/convert/package-info.java create mode 100644 src/main/java/cn/hutool/core/date/BetweenFormatter.java create mode 100644 src/main/java/cn/hutool/core/date/CalendarUtil.java create mode 100644 src/main/java/cn/hutool/core/date/ChineseDate.java create mode 100644 src/main/java/cn/hutool/core/date/DateBetween.java create mode 100644 src/main/java/cn/hutool/core/date/DateException.java create mode 100644 src/main/java/cn/hutool/core/date/DateField.java create mode 100644 src/main/java/cn/hutool/core/date/DateModifier.java create mode 100644 src/main/java/cn/hutool/core/date/DatePattern.java create mode 100644 src/main/java/cn/hutool/core/date/DateRange.java create mode 100644 src/main/java/cn/hutool/core/date/DateTime.java create mode 100644 src/main/java/cn/hutool/core/date/DateUnit.java create mode 100644 src/main/java/cn/hutool/core/date/DateUtil.java create mode 100644 src/main/java/cn/hutool/core/date/GroupTimeInterval.java create mode 100644 src/main/java/cn/hutool/core/date/LocalDateTimeUtil.java create mode 100644 src/main/java/cn/hutool/core/date/Month.java create mode 100644 src/main/java/cn/hutool/core/date/Quarter.java create mode 100644 src/main/java/cn/hutool/core/date/StopWatch.java create mode 100644 src/main/java/cn/hutool/core/date/SystemClock.java create mode 100644 src/main/java/cn/hutool/core/date/TemporalAccessorUtil.java create mode 100644 src/main/java/cn/hutool/core/date/TemporalUtil.java create mode 100644 src/main/java/cn/hutool/core/date/TimeInterval.java create mode 100644 src/main/java/cn/hutool/core/date/Week.java create mode 100644 src/main/java/cn/hutool/core/date/Zodiac.java create mode 100644 src/main/java/cn/hutool/core/date/ZoneUtil.java create mode 100644 src/main/java/cn/hutool/core/date/chinese/ChineseMonth.java create mode 100644 src/main/java/cn/hutool/core/date/chinese/GanZhi.java create mode 100644 src/main/java/cn/hutool/core/date/chinese/LunarFestival.java create mode 100644 src/main/java/cn/hutool/core/date/chinese/LunarInfo.java create mode 100644 src/main/java/cn/hutool/core/date/chinese/SolarTerms.java create mode 100644 src/main/java/cn/hutool/core/date/chinese/package-info.java create mode 100644 src/main/java/cn/hutool/core/date/format/AbstractDateBasic.java create mode 100644 src/main/java/cn/hutool/core/date/format/DateBasic.java create mode 100644 src/main/java/cn/hutool/core/date/format/DateParser.java create mode 100644 src/main/java/cn/hutool/core/date/format/DatePrinter.java create mode 100644 src/main/java/cn/hutool/core/date/format/FastDateFormat.java create mode 100644 src/main/java/cn/hutool/core/date/format/FastDateParser.java create mode 100644 src/main/java/cn/hutool/core/date/format/FastDatePrinter.java create mode 100644 src/main/java/cn/hutool/core/date/format/FormatCache.java create mode 100644 src/main/java/cn/hutool/core/date/format/GlobalCustomFormat.java create mode 100644 src/main/java/cn/hutool/core/date/format/package-info.java create mode 100644 src/main/java/cn/hutool/core/date/package-info.java create mode 100644 src/main/java/cn/hutool/core/exceptions/CheckedUtil.java create mode 100644 src/main/java/cn/hutool/core/exceptions/DependencyException.java create mode 100644 src/main/java/cn/hutool/core/exceptions/ExceptionUtil.java create mode 100644 src/main/java/cn/hutool/core/exceptions/InvocationTargetRuntimeException.java create mode 100644 src/main/java/cn/hutool/core/exceptions/NotInitedException.java create mode 100644 src/main/java/cn/hutool/core/exceptions/StatefulException.java create mode 100644 src/main/java/cn/hutool/core/exceptions/UtilException.java create mode 100644 src/main/java/cn/hutool/core/exceptions/ValidateException.java create mode 100644 src/main/java/cn/hutool/core/exceptions/package-info.java create mode 100644 src/main/java/cn/hutool/core/getter/ArrayTypeGetter.java create mode 100644 src/main/java/cn/hutool/core/getter/BasicTypeGetter.java create mode 100644 src/main/java/cn/hutool/core/getter/GroupedTypeGetter.java create mode 100644 src/main/java/cn/hutool/core/getter/ListTypeGetter.java create mode 100644 src/main/java/cn/hutool/core/getter/OptArrayTypeGetter.java create mode 100644 src/main/java/cn/hutool/core/getter/OptBasicTypeGetter.java create mode 100644 src/main/java/cn/hutool/core/getter/OptNullBasicTypeFromObjectGetter.java create mode 100644 src/main/java/cn/hutool/core/getter/OptNullBasicTypeFromStringGetter.java create mode 100644 src/main/java/cn/hutool/core/getter/OptNullBasicTypeGetter.java create mode 100644 src/main/java/cn/hutool/core/getter/package-info.java create mode 100644 src/main/java/cn/hutool/core/io/AppendableWriter.java create mode 100644 src/main/java/cn/hutool/core/io/BOMInputStream.java create mode 100644 src/main/java/cn/hutool/core/io/BomReader.java create mode 100644 src/main/java/cn/hutool/core/io/BufferUtil.java create mode 100644 src/main/java/cn/hutool/core/io/CharsetDetector.java create mode 100644 src/main/java/cn/hutool/core/io/FastByteArrayOutputStream.java create mode 100644 src/main/java/cn/hutool/core/io/FastByteBuffer.java create mode 100644 src/main/java/cn/hutool/core/io/FastStringWriter.java create mode 100644 src/main/java/cn/hutool/core/io/FileMagicNumber.java create mode 100644 src/main/java/cn/hutool/core/io/FileTypeUtil.java create mode 100644 src/main/java/cn/hutool/core/io/FileUtil.java create mode 100644 src/main/java/cn/hutool/core/io/IORuntimeException.java create mode 100644 src/main/java/cn/hutool/core/io/IoUtil.java create mode 100644 src/main/java/cn/hutool/core/io/LimitedInputStream.java create mode 100644 src/main/java/cn/hutool/core/io/LineHandler.java create mode 100644 src/main/java/cn/hutool/core/io/ManifestUtil.java create mode 100644 src/main/java/cn/hutool/core/io/NioUtil.java create mode 100644 src/main/java/cn/hutool/core/io/NullOutputStream.java create mode 100644 src/main/java/cn/hutool/core/io/StreamProgress.java create mode 100644 src/main/java/cn/hutool/core/io/ValidateObjectInputStream.java create mode 100644 src/main/java/cn/hutool/core/io/checksum/CRC16.java create mode 100644 src/main/java/cn/hutool/core/io/checksum/CRC8.java create mode 100644 src/main/java/cn/hutool/core/io/checksum/crc16/CRC16Ansi.java create mode 100644 src/main/java/cn/hutool/core/io/checksum/crc16/CRC16CCITT.java create mode 100644 src/main/java/cn/hutool/core/io/checksum/crc16/CRC16CCITTFalse.java create mode 100644 src/main/java/cn/hutool/core/io/checksum/crc16/CRC16Checksum.java create mode 100644 src/main/java/cn/hutool/core/io/checksum/crc16/CRC16DNP.java create mode 100644 src/main/java/cn/hutool/core/io/checksum/crc16/CRC16IBM.java create mode 100644 src/main/java/cn/hutool/core/io/checksum/crc16/CRC16Maxim.java create mode 100644 src/main/java/cn/hutool/core/io/checksum/crc16/CRC16Modbus.java create mode 100644 src/main/java/cn/hutool/core/io/checksum/crc16/CRC16USB.java create mode 100644 src/main/java/cn/hutool/core/io/checksum/crc16/CRC16X25.java create mode 100644 src/main/java/cn/hutool/core/io/checksum/crc16/CRC16XModem.java create mode 100644 src/main/java/cn/hutool/core/io/checksum/crc16/package-info.java create mode 100644 src/main/java/cn/hutool/core/io/checksum/package-info.java create mode 100644 src/main/java/cn/hutool/core/io/copy/ChannelCopier.java create mode 100644 src/main/java/cn/hutool/core/io/copy/IoCopier.java create mode 100644 src/main/java/cn/hutool/core/io/copy/ReaderWriterCopier.java create mode 100644 src/main/java/cn/hutool/core/io/copy/StreamCopier.java create mode 100644 src/main/java/cn/hutool/core/io/copy/package-info.java create mode 100644 src/main/java/cn/hutool/core/io/file/FileAppender.java create mode 100644 src/main/java/cn/hutool/core/io/file/FileCopier.java create mode 100644 src/main/java/cn/hutool/core/io/file/FileMode.java create mode 100644 src/main/java/cn/hutool/core/io/file/FileNameUtil.java create mode 100644 src/main/java/cn/hutool/core/io/file/FileReader.java create mode 100644 src/main/java/cn/hutool/core/io/file/FileSystemUtil.java create mode 100644 src/main/java/cn/hutool/core/io/file/FileWrapper.java create mode 100644 src/main/java/cn/hutool/core/io/file/FileWriter.java create mode 100644 src/main/java/cn/hutool/core/io/file/LineReadWatcher.java create mode 100644 src/main/java/cn/hutool/core/io/file/LineSeparator.java create mode 100644 src/main/java/cn/hutool/core/io/file/PathMover.java create mode 100644 src/main/java/cn/hutool/core/io/file/PathUtil.java create mode 100644 src/main/java/cn/hutool/core/io/file/Tailer.java create mode 100644 src/main/java/cn/hutool/core/io/file/package-info.java create mode 100644 src/main/java/cn/hutool/core/io/file/visitor/CopyVisitor.java create mode 100644 src/main/java/cn/hutool/core/io/file/visitor/DelVisitor.java create mode 100644 src/main/java/cn/hutool/core/io/file/visitor/MoveVisitor.java create mode 100644 src/main/java/cn/hutool/core/io/file/visitor/package-info.java create mode 100644 src/main/java/cn/hutool/core/io/package-info.java create mode 100644 src/main/java/cn/hutool/core/io/resource/BytesResource.java create mode 100644 src/main/java/cn/hutool/core/io/resource/CharSequenceResource.java create mode 100644 src/main/java/cn/hutool/core/io/resource/ClassPathResource.java create mode 100644 src/main/java/cn/hutool/core/io/resource/FileResource.java create mode 100644 src/main/java/cn/hutool/core/io/resource/InputStreamResource.java create mode 100644 src/main/java/cn/hutool/core/io/resource/MultiFileResource.java create mode 100644 src/main/java/cn/hutool/core/io/resource/MultiResource.java create mode 100644 src/main/java/cn/hutool/core/io/resource/NoResourceException.java create mode 100644 src/main/java/cn/hutool/core/io/resource/Resource.java create mode 100644 src/main/java/cn/hutool/core/io/resource/ResourceUtil.java create mode 100644 src/main/java/cn/hutool/core/io/resource/StringResource.java create mode 100644 src/main/java/cn/hutool/core/io/resource/UrlResource.java create mode 100644 src/main/java/cn/hutool/core/io/resource/VfsResource.java create mode 100644 src/main/java/cn/hutool/core/io/resource/WebAppResource.java create mode 100644 src/main/java/cn/hutool/core/io/resource/package-info.java create mode 100644 src/main/java/cn/hutool/core/io/unit/DataSize.java create mode 100644 src/main/java/cn/hutool/core/io/unit/DataSizeUtil.java create mode 100644 src/main/java/cn/hutool/core/io/unit/DataUnit.java create mode 100644 src/main/java/cn/hutool/core/io/unit/package-info.java create mode 100644 src/main/java/cn/hutool/core/io/watch/SimpleWatcher.java create mode 100644 src/main/java/cn/hutool/core/io/watch/WatchAction.java create mode 100644 src/main/java/cn/hutool/core/io/watch/WatchException.java create mode 100644 src/main/java/cn/hutool/core/io/watch/WatchKind.java create mode 100644 src/main/java/cn/hutool/core/io/watch/WatchMonitor.java create mode 100644 src/main/java/cn/hutool/core/io/watch/WatchServer.java create mode 100644 src/main/java/cn/hutool/core/io/watch/WatchUtil.java create mode 100644 src/main/java/cn/hutool/core/io/watch/Watcher.java create mode 100644 src/main/java/cn/hutool/core/io/watch/package-info.java create mode 100644 src/main/java/cn/hutool/core/io/watch/watchers/DelayWatcher.java create mode 100644 src/main/java/cn/hutool/core/io/watch/watchers/IgnoreWatcher.java create mode 100644 src/main/java/cn/hutool/core/io/watch/watchers/WatcherChain.java create mode 100644 src/main/java/cn/hutool/core/io/watch/watchers/package-info.java create mode 100644 src/main/java/cn/hutool/core/lang/Assert.java create mode 100644 src/main/java/cn/hutool/core/lang/Chain.java create mode 100644 src/main/java/cn/hutool/core/lang/ClassScanner.java create mode 100644 src/main/java/cn/hutool/core/lang/ConsistentHash.java create mode 100644 src/main/java/cn/hutool/core/lang/Console.java create mode 100644 src/main/java/cn/hutool/core/lang/ConsoleTable.java create mode 100644 src/main/java/cn/hutool/core/lang/DefaultSegment.java create mode 100644 src/main/java/cn/hutool/core/lang/Dict.java create mode 100644 src/main/java/cn/hutool/core/lang/Editor.java create mode 100644 src/main/java/cn/hutool/core/lang/EnumItem.java create mode 100644 src/main/java/cn/hutool/core/lang/Filter.java create mode 100644 src/main/java/cn/hutool/core/lang/JarClassLoader.java create mode 100644 src/main/java/cn/hutool/core/lang/Matcher.java create mode 100644 src/main/java/cn/hutool/core/lang/Opt.java create mode 100644 src/main/java/cn/hutool/core/lang/Pair.java create mode 100644 src/main/java/cn/hutool/core/lang/ParameterizedTypeImpl.java create mode 100644 src/main/java/cn/hutool/core/lang/PatternPool.java create mode 100644 src/main/java/cn/hutool/core/lang/Range.java create mode 100644 src/main/java/cn/hutool/core/lang/RegexPool.java create mode 100644 src/main/java/cn/hutool/core/lang/Replacer.java create mode 100644 src/main/java/cn/hutool/core/lang/ResourceClassLoader.java create mode 100644 src/main/java/cn/hutool/core/lang/Segment.java create mode 100644 src/main/java/cn/hutool/core/lang/SimpleCache.java create mode 100644 src/main/java/cn/hutool/core/lang/Singleton.java create mode 100644 src/main/java/cn/hutool/core/lang/Tuple.java create mode 100644 src/main/java/cn/hutool/core/lang/TypeReference.java create mode 100644 src/main/java/cn/hutool/core/lang/UUID.java create mode 100644 src/main/java/cn/hutool/core/lang/Validator.java create mode 100644 src/main/java/cn/hutool/core/lang/WeightRandom.java create mode 100644 src/main/java/cn/hutool/core/lang/ansi/Ansi8BitColor.java create mode 100644 src/main/java/cn/hutool/core/lang/ansi/AnsiBackground.java create mode 100644 src/main/java/cn/hutool/core/lang/ansi/AnsiColor.java create mode 100644 src/main/java/cn/hutool/core/lang/ansi/AnsiElement.java create mode 100644 src/main/java/cn/hutool/core/lang/ansi/AnsiEncoder.java create mode 100644 src/main/java/cn/hutool/core/lang/ansi/AnsiStyle.java create mode 100644 src/main/java/cn/hutool/core/lang/ansi/ForeOrBack.java create mode 100644 src/main/java/cn/hutool/core/lang/ansi/package-info.java create mode 100644 src/main/java/cn/hutool/core/lang/caller/Caller.java create mode 100644 src/main/java/cn/hutool/core/lang/caller/CallerUtil.java create mode 100644 src/main/java/cn/hutool/core/lang/caller/SecurityManagerCaller.java create mode 100644 src/main/java/cn/hutool/core/lang/caller/StackTraceCaller.java create mode 100644 src/main/java/cn/hutool/core/lang/caller/package-info.java create mode 100644 src/main/java/cn/hutool/core/lang/copier/Copier.java create mode 100644 src/main/java/cn/hutool/core/lang/copier/SrcToDestCopier.java create mode 100644 src/main/java/cn/hutool/core/lang/copier/package-info.java create mode 100644 src/main/java/cn/hutool/core/lang/func/Consumer3.java create mode 100644 src/main/java/cn/hutool/core/lang/func/Func.java create mode 100644 src/main/java/cn/hutool/core/lang/func/Func0.java create mode 100644 src/main/java/cn/hutool/core/lang/func/Func1.java create mode 100644 src/main/java/cn/hutool/core/lang/func/LambdaUtil.java create mode 100644 src/main/java/cn/hutool/core/lang/func/Supplier1.java create mode 100644 src/main/java/cn/hutool/core/lang/func/Supplier2.java create mode 100644 src/main/java/cn/hutool/core/lang/func/Supplier3.java create mode 100644 src/main/java/cn/hutool/core/lang/func/Supplier4.java create mode 100644 src/main/java/cn/hutool/core/lang/func/Supplier5.java create mode 100644 src/main/java/cn/hutool/core/lang/func/VoidFunc.java create mode 100644 src/main/java/cn/hutool/core/lang/func/VoidFunc0.java create mode 100644 src/main/java/cn/hutool/core/lang/func/VoidFunc1.java create mode 100644 src/main/java/cn/hutool/core/lang/func/package-info.java create mode 100644 src/main/java/cn/hutool/core/lang/generator/Generator.java create mode 100644 src/main/java/cn/hutool/core/lang/generator/ObjectGenerator.java create mode 100644 src/main/java/cn/hutool/core/lang/generator/package-info.java create mode 100644 src/main/java/cn/hutool/core/lang/hash/CityHash.java create mode 100644 src/main/java/cn/hutool/core/lang/hash/Hash.java create mode 100644 src/main/java/cn/hutool/core/lang/hash/Hash128.java create mode 100644 src/main/java/cn/hutool/core/lang/hash/Hash32.java create mode 100644 src/main/java/cn/hutool/core/lang/hash/Hash64.java create mode 100644 src/main/java/cn/hutool/core/lang/hash/KetamaHash.java create mode 100644 src/main/java/cn/hutool/core/lang/hash/MetroHash.java create mode 100644 src/main/java/cn/hutool/core/lang/hash/MurmurHash.java create mode 100644 src/main/java/cn/hutool/core/lang/hash/Number128.java create mode 100644 src/main/java/cn/hutool/core/lang/hash/package-info.java create mode 100644 src/main/java/cn/hutool/core/lang/id/NanoId.java create mode 100644 src/main/java/cn/hutool/core/lang/id/package-info.java create mode 100644 src/main/java/cn/hutool/core/lang/intern/InternUtil.java create mode 100644 src/main/java/cn/hutool/core/lang/intern/Interner.java create mode 100644 src/main/java/cn/hutool/core/lang/intern/JdkStringInterner.java create mode 100644 src/main/java/cn/hutool/core/lang/intern/WeakInterner.java create mode 100644 src/main/java/cn/hutool/core/lang/intern/package-info.java create mode 100644 src/main/java/cn/hutool/core/lang/loader/AtomicLoader.java create mode 100644 src/main/java/cn/hutool/core/lang/loader/LazyFunLoader.java create mode 100644 src/main/java/cn/hutool/core/lang/loader/LazyLoader.java create mode 100644 src/main/java/cn/hutool/core/lang/loader/Loader.java create mode 100644 src/main/java/cn/hutool/core/lang/loader/package-info.java create mode 100644 src/main/java/cn/hutool/core/lang/mutable/Mutable.java create mode 100644 src/main/java/cn/hutool/core/lang/mutable/MutableBool.java create mode 100644 src/main/java/cn/hutool/core/lang/mutable/MutableByte.java create mode 100644 src/main/java/cn/hutool/core/lang/mutable/MutableDouble.java create mode 100644 src/main/java/cn/hutool/core/lang/mutable/MutableFloat.java create mode 100644 src/main/java/cn/hutool/core/lang/mutable/MutableInt.java create mode 100644 src/main/java/cn/hutool/core/lang/mutable/MutableLong.java create mode 100644 src/main/java/cn/hutool/core/lang/mutable/MutableObj.java create mode 100644 src/main/java/cn/hutool/core/lang/mutable/MutablePair.java create mode 100644 src/main/java/cn/hutool/core/lang/mutable/MutableShort.java create mode 100644 src/main/java/cn/hutool/core/lang/mutable/package-info.java create mode 100644 src/main/java/cn/hutool/core/lang/package-info.java create mode 100644 src/main/java/cn/hutool/core/lang/reflect/ActualTypeMapperPool.java create mode 100644 src/main/java/cn/hutool/core/lang/reflect/LookupFactory.java create mode 100644 src/main/java/cn/hutool/core/lang/reflect/MethodHandleUtil.java create mode 100644 src/main/java/cn/hutool/core/lang/reflect/package-info.java create mode 100644 src/main/java/cn/hutool/core/lang/tree/Node.java create mode 100644 src/main/java/cn/hutool/core/lang/tree/Tree.java create mode 100644 src/main/java/cn/hutool/core/lang/tree/TreeBuilder.java create mode 100644 src/main/java/cn/hutool/core/lang/tree/TreeNode.java create mode 100644 src/main/java/cn/hutool/core/lang/tree/TreeNodeConfig.java create mode 100644 src/main/java/cn/hutool/core/lang/tree/TreeUtil.java create mode 100644 src/main/java/cn/hutool/core/lang/tree/package-info.java create mode 100644 src/main/java/cn/hutool/core/lang/tree/parser/DefaultNodeParser.java create mode 100644 src/main/java/cn/hutool/core/lang/tree/parser/NodeParser.java create mode 100644 src/main/java/cn/hutool/core/map/AbsEntry.java create mode 100644 src/main/java/cn/hutool/core/map/BiMap.java create mode 100644 src/main/java/cn/hutool/core/map/CamelCaseLinkedMap.java create mode 100644 src/main/java/cn/hutool/core/map/CamelCaseMap.java create mode 100644 src/main/java/cn/hutool/core/map/CaseInsensitiveLinkedMap.java create mode 100644 src/main/java/cn/hutool/core/map/CaseInsensitiveMap.java create mode 100644 src/main/java/cn/hutool/core/map/CaseInsensitiveTreeMap.java create mode 100644 src/main/java/cn/hutool/core/map/CustomKeyMap.java create mode 100644 src/main/java/cn/hutool/core/map/FixedLinkedHashMap.java create mode 100644 src/main/java/cn/hutool/core/map/ForestMap.java create mode 100644 src/main/java/cn/hutool/core/map/FuncKeyMap.java create mode 100644 src/main/java/cn/hutool/core/map/FuncMap.java create mode 100644 src/main/java/cn/hutool/core/map/LinkedForestMap.java create mode 100644 src/main/java/cn/hutool/core/map/MapBuilder.java create mode 100644 src/main/java/cn/hutool/core/map/MapProxy.java create mode 100644 src/main/java/cn/hutool/core/map/MapUtil.java create mode 100644 src/main/java/cn/hutool/core/map/MapWrapper.java create mode 100644 src/main/java/cn/hutool/core/map/ReferenceConcurrentMap.java create mode 100644 src/main/java/cn/hutool/core/map/SafeConcurrentHashMap.java create mode 100644 src/main/java/cn/hutool/core/map/TableMap.java create mode 100644 src/main/java/cn/hutool/core/map/TolerantMap.java create mode 100644 src/main/java/cn/hutool/core/map/TransMap.java create mode 100644 src/main/java/cn/hutool/core/map/TreeEntry.java create mode 100644 src/main/java/cn/hutool/core/map/WeakConcurrentMap.java create mode 100644 src/main/java/cn/hutool/core/map/multi/AbsCollValueMap.java create mode 100644 src/main/java/cn/hutool/core/map/multi/AbsTable.java create mode 100644 src/main/java/cn/hutool/core/map/multi/CollectionValueMap.java create mode 100644 src/main/java/cn/hutool/core/map/multi/ListValueMap.java create mode 100644 src/main/java/cn/hutool/core/map/multi/RowKeyTable.java create mode 100644 src/main/java/cn/hutool/core/map/multi/SetValueMap.java create mode 100644 src/main/java/cn/hutool/core/map/multi/Table.java create mode 100644 src/main/java/cn/hutool/core/map/multi/package-info.java create mode 100644 src/main/java/cn/hutool/core/map/package-info.java create mode 100644 src/main/java/cn/hutool/core/math/Arrangement.java create mode 100644 src/main/java/cn/hutool/core/math/BitStatusUtil.java create mode 100644 src/main/java/cn/hutool/core/math/Calculator.java create mode 100644 src/main/java/cn/hutool/core/math/Combination.java create mode 100644 src/main/java/cn/hutool/core/math/MathUtil.java create mode 100644 src/main/java/cn/hutool/core/math/Money.java create mode 100644 src/main/java/cn/hutool/core/math/package-info.java create mode 100644 src/main/java/cn/hutool/core/net/DefaultTrustManager.java create mode 100644 src/main/java/cn/hutool/core/net/FormUrlencoded.java create mode 100644 src/main/java/cn/hutool/core/net/Ipv4Util.java create mode 100644 src/main/java/cn/hutool/core/net/MaskBit.java create mode 100644 src/main/java/cn/hutool/core/net/NetUtil.java create mode 100644 src/main/java/cn/hutool/core/net/PassAuth.java create mode 100644 src/main/java/cn/hutool/core/net/RFC3986.java create mode 100644 src/main/java/cn/hutool/core/net/SSLContextBuilder.java create mode 100644 src/main/java/cn/hutool/core/net/SSLProtocols.java create mode 100644 src/main/java/cn/hutool/core/net/SSLUtil.java create mode 100644 src/main/java/cn/hutool/core/net/URLDecoder.java create mode 100644 src/main/java/cn/hutool/core/net/URLEncodeUtil.java create mode 100644 src/main/java/cn/hutool/core/net/URLEncoder.java create mode 100644 src/main/java/cn/hutool/core/net/UserPassAuthenticator.java create mode 100644 src/main/java/cn/hutool/core/net/multipart/MultipartFormData.java create mode 100644 src/main/java/cn/hutool/core/net/multipart/MultipartRequestInputStream.java create mode 100644 src/main/java/cn/hutool/core/net/multipart/UploadFile.java create mode 100644 src/main/java/cn/hutool/core/net/multipart/UploadFileHeader.java create mode 100644 src/main/java/cn/hutool/core/net/multipart/UploadSetting.java create mode 100644 src/main/java/cn/hutool/core/net/multipart/package-info.java create mode 100644 src/main/java/cn/hutool/core/net/package-info.java create mode 100644 src/main/java/cn/hutool/core/net/url/UrlBuilder.java create mode 100644 src/main/java/cn/hutool/core/net/url/UrlPath.java create mode 100644 src/main/java/cn/hutool/core/net/url/UrlQuery.java create mode 100644 src/main/java/cn/hutool/core/net/url/package-info.java create mode 100644 src/main/java/cn/hutool/core/package-info.java create mode 100644 src/main/java/cn/hutool/core/stream/CollectorUtil.java create mode 100644 src/main/java/cn/hutool/core/stream/SimpleCollector.java create mode 100644 src/main/java/cn/hutool/core/stream/StreamUtil.java create mode 100644 src/main/java/cn/hutool/core/stream/package-info.java create mode 100644 src/main/java/cn/hutool/core/text/ASCIIStrCache.java create mode 100644 src/main/java/cn/hutool/core/text/AntPathMatcher.java create mode 100644 src/main/java/cn/hutool/core/text/CharPool.java create mode 100644 src/main/java/cn/hutool/core/text/CharSequenceUtil.java create mode 100644 src/main/java/cn/hutool/core/text/NamingCase.java create mode 100644 src/main/java/cn/hutool/core/text/PasswdStrength.java create mode 100644 src/main/java/cn/hutool/core/text/Simhash.java create mode 100644 src/main/java/cn/hutool/core/text/StrBuilder.java create mode 100644 src/main/java/cn/hutool/core/text/StrFormatter.java create mode 100644 src/main/java/cn/hutool/core/text/StrJoiner.java create mode 100644 src/main/java/cn/hutool/core/text/StrMatcher.java create mode 100644 src/main/java/cn/hutool/core/text/StrPool.java create mode 100644 src/main/java/cn/hutool/core/text/StrSplitter.java create mode 100644 src/main/java/cn/hutool/core/text/TextSimilarity.java create mode 100644 src/main/java/cn/hutool/core/text/UnicodeUtil.java create mode 100644 src/main/java/cn/hutool/core/text/csv/CsvBaseReader.java create mode 100644 src/main/java/cn/hutool/core/text/csv/CsvConfig.java create mode 100644 src/main/java/cn/hutool/core/text/csv/CsvData.java create mode 100644 src/main/java/cn/hutool/core/text/csv/CsvParser.java create mode 100644 src/main/java/cn/hutool/core/text/csv/CsvReadConfig.java create mode 100644 src/main/java/cn/hutool/core/text/csv/CsvReader.java create mode 100644 src/main/java/cn/hutool/core/text/csv/CsvRow.java create mode 100644 src/main/java/cn/hutool/core/text/csv/CsvRowHandler.java create mode 100644 src/main/java/cn/hutool/core/text/csv/CsvUtil.java create mode 100644 src/main/java/cn/hutool/core/text/csv/CsvWriteConfig.java create mode 100644 src/main/java/cn/hutool/core/text/csv/CsvWriter.java create mode 100644 src/main/java/cn/hutool/core/text/csv/package-info.java create mode 100644 src/main/java/cn/hutool/core/text/escape/Html4Escape.java create mode 100644 src/main/java/cn/hutool/core/text/escape/Html4Unescape.java create mode 100644 src/main/java/cn/hutool/core/text/escape/InternalEscapeUtil.java create mode 100644 src/main/java/cn/hutool/core/text/escape/NumericEntityUnescaper.java create mode 100644 src/main/java/cn/hutool/core/text/escape/XmlEscape.java create mode 100644 src/main/java/cn/hutool/core/text/escape/XmlUnescape.java create mode 100644 src/main/java/cn/hutool/core/text/escape/package-info.java create mode 100644 src/main/java/cn/hutool/core/text/finder/CharFinder.java create mode 100644 src/main/java/cn/hutool/core/text/finder/CharMatcherFinder.java create mode 100644 src/main/java/cn/hutool/core/text/finder/Finder.java create mode 100644 src/main/java/cn/hutool/core/text/finder/LengthFinder.java create mode 100644 src/main/java/cn/hutool/core/text/finder/PatternFinder.java create mode 100644 src/main/java/cn/hutool/core/text/finder/StrFinder.java create mode 100644 src/main/java/cn/hutool/core/text/finder/TextFinder.java create mode 100644 src/main/java/cn/hutool/core/text/finder/package-info.java create mode 100644 src/main/java/cn/hutool/core/text/package-info.java create mode 100644 src/main/java/cn/hutool/core/text/replacer/LookupReplacer.java create mode 100644 src/main/java/cn/hutool/core/text/replacer/ReplacerChain.java create mode 100644 src/main/java/cn/hutool/core/text/replacer/StrReplacer.java create mode 100644 src/main/java/cn/hutool/core/text/replacer/package-info.java create mode 100644 src/main/java/cn/hutool/core/text/split/SplitIter.java create mode 100644 src/main/java/cn/hutool/core/thread/AsyncUtil.java create mode 100644 src/main/java/cn/hutool/core/thread/BlockPolicy.java create mode 100644 src/main/java/cn/hutool/core/thread/ConcurrencyTester.java create mode 100644 src/main/java/cn/hutool/core/thread/DelegatedExecutorService.java create mode 100644 src/main/java/cn/hutool/core/thread/ExecutorBuilder.java create mode 100644 src/main/java/cn/hutool/core/thread/FinalizableDelegatedExecutorService.java create mode 100644 src/main/java/cn/hutool/core/thread/GlobalThreadPool.java create mode 100644 src/main/java/cn/hutool/core/thread/NamedThreadFactory.java create mode 100644 src/main/java/cn/hutool/core/thread/RejectPolicy.java create mode 100644 src/main/java/cn/hutool/core/thread/SemaphoreRunnable.java create mode 100644 src/main/java/cn/hutool/core/thread/SyncFinisher.java create mode 100644 src/main/java/cn/hutool/core/thread/ThreadException.java create mode 100644 src/main/java/cn/hutool/core/thread/ThreadFactoryBuilder.java create mode 100644 src/main/java/cn/hutool/core/thread/ThreadUtil.java create mode 100644 src/main/java/cn/hutool/core/thread/lock/LockUtil.java create mode 100644 src/main/java/cn/hutool/core/thread/lock/NoLock.java create mode 100644 src/main/java/cn/hutool/core/thread/lock/NoReadWriteLock.java create mode 100644 src/main/java/cn/hutool/core/thread/lock/package-info.java create mode 100644 src/main/java/cn/hutool/core/thread/package-info.java create mode 100644 src/main/java/cn/hutool/core/thread/threadlocal/NamedInheritableThreadLocal.java create mode 100644 src/main/java/cn/hutool/core/thread/threadlocal/NamedThreadLocal.java create mode 100644 src/main/java/cn/hutool/core/thread/threadlocal/package-info.java create mode 100644 src/main/java/cn/hutool/core/util/ArrayUtil.java create mode 100644 src/main/java/cn/hutool/core/util/BooleanUtil.java create mode 100644 src/main/java/cn/hutool/core/util/ByteUtil.java create mode 100644 src/main/java/cn/hutool/core/util/CharUtil.java create mode 100644 src/main/java/cn/hutool/core/util/CharsetUtil.java create mode 100644 src/main/java/cn/hutool/core/util/ClassLoaderUtil.java create mode 100644 src/main/java/cn/hutool/core/util/ClassUtil.java create mode 100644 src/main/java/cn/hutool/core/util/CoordinateUtil.java create mode 100644 src/main/java/cn/hutool/core/util/CreditCodeUtil.java create mode 100644 src/main/java/cn/hutool/core/util/DesensitizedUtil.java create mode 100644 src/main/java/cn/hutool/core/util/EnumUtil.java create mode 100644 src/main/java/cn/hutool/core/util/EscapeUtil.java create mode 100644 src/main/java/cn/hutool/core/util/HashUtil.java create mode 100644 src/main/java/cn/hutool/core/util/HexUtil.java create mode 100644 src/main/java/cn/hutool/core/util/IdcardUtil.java create mode 100644 src/main/java/cn/hutool/core/util/ModifierUtil.java create mode 100644 src/main/java/cn/hutool/core/util/NumberUtil.java create mode 100644 src/main/java/cn/hutool/core/util/ObjUtil.java create mode 100644 src/main/java/cn/hutool/core/util/ObjectUtil.java create mode 100644 src/main/java/cn/hutool/core/util/PageUtil.java create mode 100644 src/main/java/cn/hutool/core/util/PhoneUtil.java create mode 100644 src/main/java/cn/hutool/core/util/PrimitiveArrayUtil.java create mode 100644 src/main/java/cn/hutool/core/util/RadixUtil.java create mode 100644 src/main/java/cn/hutool/core/util/RandomUtil.java create mode 100644 src/main/java/cn/hutool/core/util/ReUtil.java create mode 100644 src/main/java/cn/hutool/core/util/ReferenceUtil.java create mode 100644 src/main/java/cn/hutool/core/util/ReflectUtil.java create mode 100644 src/main/java/cn/hutool/core/util/SerializeUtil.java create mode 100644 src/main/java/cn/hutool/core/util/ServiceLoaderUtil.java create mode 100644 src/main/java/cn/hutool/core/util/StrUtil.java create mode 100644 src/main/java/cn/hutool/core/util/SystemPropsUtil.java create mode 100644 src/main/java/cn/hutool/core/util/TypeUtil.java create mode 100644 src/main/java/cn/hutool/core/util/URLUtil.java create mode 100644 src/main/java/cn/hutool/core/util/ZipUtil.java create mode 100644 src/main/java/cn/hutool/core/util/package-info.java create mode 100644 src/main/java/com/keyware/regtool/AESUtil.java create mode 100644 src/main/java/com/keyware/regtool/Application.java create mode 100644 src/main/java/com/keyware/regtool/Main.java create mode 100644 src/main/java/com/keyware/regtool/RegInfo.java create mode 100644 src/main/java/com/keyware/regtool/WebViewController.java create mode 100644 src/main/java/module-info.java create mode 100644 src/main/resources/META-INF/MANIFEST.MF create mode 100644 src/main/resources/logo.ico create mode 100644 src/main/resources/logo.png create mode 100644 src/main/resources/scripts/layui/css/layui.css create mode 100644 src/main/resources/scripts/layui/font/iconfont.eot create mode 100644 src/main/resources/scripts/layui/font/iconfont.svg create mode 100644 src/main/resources/scripts/layui/font/iconfont.ttf create mode 100644 src/main/resources/scripts/layui/font/iconfont.woff create mode 100644 src/main/resources/scripts/layui/font/iconfont.woff2 create mode 100644 src/main/resources/scripts/layui/layui.js create mode 100644 src/main/resources/style/index.css create mode 100644 src/main/resources/template/index.html diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5ff6309 --- /dev/null +++ b/.gitignore @@ -0,0 +1,38 @@ +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### IntelliJ IDEA ### +.idea/modules.xml +.idea/jarRepositories.xml +.idea/compiler.xml +.idea/libraries/ +*.iws +*.iml +*.ipr + +### Eclipse ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ + +### Mac OS ### +.DS_Store \ No newline at end of file diff --git a/.mvn/wrapper/maven-wrapper.jar b/.mvn/wrapper/maven-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..c1dd12f17644411d6e840bd5a10c6ecda0175f18 GIT binary patch literal 58727 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%0ANsA|A3-jj|r`KAo%oTajX3>^E` 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 YM{}PByk10KD_nuQ4E7Du?}+~TKh4V)`~Uy| literal 0 HcmV?d00001 diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000..40ca015 --- /dev/null +++ b/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,2 @@ +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.8.5/apache-maven-3.8.5-bin.zip +wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar \ No newline at end of file diff --git a/mvnw b/mvnw new file mode 100644 index 0000000..8a8fb22 --- /dev/null +++ b/mvnw @@ -0,0 +1,316 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# 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 +# +# https://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. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Maven Start Up Batch script +# +# Required ENV vars: +# ------------------ +# JAVA_HOME - location of a JDK home dir +# +# Optional ENV vars +# ----------------- +# M2_HOME - location of maven2's installed home dir +# MAVEN_OPTS - parameters passed to the Java VM when running Maven +# e.g. to debug Maven itself, use +# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +# MAVEN_SKIP_RC - flag to disable loading of mavenrc files +# ---------------------------------------------------------------------------- + +if [ -z "$MAVEN_SKIP_RC" ] ; then + + if [ -f /usr/local/etc/mavenrc ] ; then + . /usr/local/etc/mavenrc + fi + + if [ -f /etc/mavenrc ] ; then + . /etc/mavenrc + fi + + if [ -f "$HOME/.mavenrc" ] ; then + . "$HOME/.mavenrc" + fi + +fi + +# OS specific support. $var _must_ be set to either true or false. +cygwin=false; +darwin=false; +mingw=false +case "`uname`" in + CYGWIN*) cygwin=true ;; + MINGW*) mingw=true;; + Darwin*) darwin=true + # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home + # See https://developer.apple.com/library/mac/qa/qa1170/_index.html + if [ -z "$JAVA_HOME" ]; then + if [ -x "/usr/libexec/java_home" ]; then + export JAVA_HOME="`/usr/libexec/java_home`" + else + export JAVA_HOME="/Library/Java/Home" + fi + fi + ;; +esac + +if [ -z "$JAVA_HOME" ] ; then + if [ -r /etc/gentoo-release ] ; then + JAVA_HOME=`java-config --jre-home` + fi +fi + +if [ -z "$M2_HOME" ] ; then + ## resolve links - $0 may be a link to maven's home + PRG="$0" + + # need this for relative symlinks + while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG="`dirname "$PRG"`/$link" + fi + done + + saveddir=`pwd` + + M2_HOME=`dirname "$PRG"`/.. + + # make it fully qualified + M2_HOME=`cd "$M2_HOME" && pwd` + + cd "$saveddir" + # echo Using m2 at $M2_HOME +fi + +# For Cygwin, ensure paths are in UNIX format before anything is touched +if $cygwin ; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --unix "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --unix "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --unix "$CLASSPATH"` +fi + +# For Mingw, ensure paths are in UNIX format before anything is touched +if $mingw ; then + [ -n "$M2_HOME" ] && + M2_HOME="`(cd "$M2_HOME"; pwd)`" + [ -n "$JAVA_HOME" ] && + JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" +fi + +if [ -z "$JAVA_HOME" ]; then + javaExecutable="`which javac`" + if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then + # readlink(1) is not available as standard on Solaris 10. + readLink=`which readlink` + if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then + if $darwin ; then + javaHome="`dirname \"$javaExecutable\"`" + javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" + else + javaExecutable="`readlink -f \"$javaExecutable\"`" + fi + javaHome="`dirname \"$javaExecutable\"`" + javaHome=`expr "$javaHome" : '\(.*\)/bin'` + JAVA_HOME="$javaHome" + export JAVA_HOME + fi + fi +fi + +if [ -z "$JAVACMD" ] ; then + if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + else + JAVACMD="`\\unset -f command; \\command -v java`" + fi +fi + +if [ ! -x "$JAVACMD" ] ; then + echo "Error: JAVA_HOME is not defined correctly." >&2 + echo " We cannot execute $JAVACMD" >&2 + exit 1 +fi + +if [ -z "$JAVA_HOME" ] ; then + echo "Warning: JAVA_HOME environment variable is not set." +fi + +CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher + +# traverses directory structure from process work directory to filesystem root +# first directory with .mvn subdirectory is considered project base directory +find_maven_basedir() { + + if [ -z "$1" ] + then + echo "Path not specified to find_maven_basedir" + return 1 + fi + + basedir="$1" + wdir="$1" + while [ "$wdir" != '/' ] ; do + if [ -d "$wdir"/.mvn ] ; then + basedir=$wdir + break + fi + # workaround for JBEAP-8937 (on Solaris 10/Sparc) + if [ -d "${wdir}" ]; then + wdir=`cd "$wdir/.."; pwd` + fi + # end of workaround + done + echo "${basedir}" +} + +# concatenates all lines of a file +concat_lines() { + if [ -f "$1" ]; then + echo "$(tr -s '\n' ' ' < "$1")" + fi +} + +BASE_DIR=`find_maven_basedir "$(pwd)"` +if [ -z "$BASE_DIR" ]; then + exit 1; +fi + +########################################################################################## +# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +# This allows using the maven wrapper in projects that prohibit checking in binary data. +########################################################################################## +if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found .mvn/wrapper/maven-wrapper.jar" + fi +else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." + fi + if [ -n "$MVNW_REPOURL" ]; then + jarUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" + else + jarUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" + fi + while IFS="=" read key value; do + case "$key" in (wrapperUrl) jarUrl="$value"; break ;; + esac + done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" + if [ "$MVNW_VERBOSE" = true ]; then + echo "Downloading from: $jarUrl" + fi + wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" + if $cygwin; then + wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"` + fi + + if command -v wget > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found wget ... using wget" + fi + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + wget "$jarUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + else + wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + fi + elif command -v curl > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found curl ... using curl" + fi + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + curl -o "$wrapperJarPath" "$jarUrl" -f + else + curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f + fi + + else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Falling back to using Java to download" + fi + javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" + # For Cygwin, switch paths to Windows format before running javac + if $cygwin; then + javaClass=`cygpath --path --windows "$javaClass"` + fi + if [ -e "$javaClass" ]; then + if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Compiling MavenWrapperDownloader.java ..." + fi + # Compiling the Java class + ("$JAVA_HOME/bin/javac" "$javaClass") + fi + if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + # Running the downloader + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Running MavenWrapperDownloader.java ..." + fi + ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") + fi + fi + fi +fi +########################################################################################## +# End of extension +########################################################################################## + +export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} +if [ "$MVNW_VERBOSE" = true ]; then + echo $MAVEN_PROJECTBASEDIR +fi +MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" + +# For Cygwin, switch paths to Windows format before running java +if $cygwin; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --path --windows "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --windows "$CLASSPATH"` + [ -n "$MAVEN_PROJECTBASEDIR" ] && + MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` +fi + +# Provide a "standardized" way to retrieve the CLI args that will +# work with both Windows and non-Windows executions. +MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" +export MAVEN_CMD_LINE_ARGS + +WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +exec "$JAVACMD" \ + $MAVEN_OPTS \ + $MAVEN_DEBUG_OPTS \ + -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ + "-Dmaven.home=${M2_HOME}" \ + "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/mvnw.cmd b/mvnw.cmd new file mode 100644 index 0000000..1d8ab01 --- /dev/null +++ b/mvnw.cmd @@ -0,0 +1,188 @@ +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM https://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Maven Start Up Batch script +@REM +@REM Required ENV vars: +@REM JAVA_HOME - location of a JDK home dir +@REM +@REM Optional ENV vars +@REM M2_HOME - location of maven2's installed home dir +@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending +@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven +@REM e.g. to debug Maven itself, use +@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files +@REM ---------------------------------------------------------------------------- + +@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' +@echo off +@REM set title of command window +title %0 +@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' +@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% + +@REM set %HOME% to equivalent of $HOME +if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") + +@REM Execute a user defined script before this one +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre +@REM check for pre script, once with legacy .bat ending and once with .cmd ending +if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %* +if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %* +:skipRcPre + +@setlocal + +set ERROR_CODE=0 + +@REM To isolate internal variables from possible post scripts, we use another setlocal +@setlocal + +@REM ==== START VALIDATION ==== +if not "%JAVA_HOME%" == "" goto OkJHome + +echo. +echo Error: JAVA_HOME not found in your environment. >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +:OkJHome +if exist "%JAVA_HOME%\bin\java.exe" goto init + +echo. +echo Error: JAVA_HOME is set to an invalid directory. >&2 +echo JAVA_HOME = "%JAVA_HOME%" >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +@REM ==== END VALIDATION ==== + +:init + +@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". +@REM Fallback to current working directory if not found. + +set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% +IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir + +set EXEC_DIR=%CD% +set WDIR=%EXEC_DIR% +:findBaseDir +IF EXIST "%WDIR%"\.mvn goto baseDirFound +cd .. +IF "%WDIR%"=="%CD%" goto baseDirNotFound +set WDIR=%CD% +goto findBaseDir + +:baseDirFound +set MAVEN_PROJECTBASEDIR=%WDIR% +cd "%EXEC_DIR%" +goto endDetectBaseDir + +:baseDirNotFound +set MAVEN_PROJECTBASEDIR=%EXEC_DIR% +cd "%EXEC_DIR%" + +:endDetectBaseDir + +IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig + +@setlocal EnableExtensions EnableDelayedExpansion +for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a +@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% + +:endReadAdditionalConfig + +SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" +set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" +set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" + +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B +) + +@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +@REM This allows using the maven wrapper in projects that prohibit checking in binary data. +if exist %WRAPPER_JAR% ( + if "%MVNW_VERBOSE%" == "true" ( + echo Found %WRAPPER_JAR% + ) +) else ( + if not "%MVNW_REPOURL%" == "" ( + SET DOWNLOAD_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" + ) + if "%MVNW_VERBOSE%" == "true" ( + echo Couldn't find %WRAPPER_JAR%, downloading it ... + echo Downloading from: %DOWNLOAD_URL% + ) + + powershell -Command "&{"^ + "$webclient = new-object System.Net.WebClient;"^ + "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ + "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ + "}"^ + "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^ + "}" + if "%MVNW_VERBOSE%" == "true" ( + echo Finished downloading %WRAPPER_JAR% + ) +) +@REM End of extension + +@REM Provide a "standardized" way to retrieve the CLI args that will +@REM work with both Windows and non-Windows executions. +set MAVEN_CMD_LINE_ARGS=%* + +%MAVEN_JAVA_EXE% ^ + %JVM_CONFIG_MAVEN_PROPS% ^ + %MAVEN_OPTS% ^ + %MAVEN_DEBUG_OPTS% ^ + -classpath %WRAPPER_JAR% ^ + "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^ + %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +if ERRORLEVEL 1 goto error +goto end + +:error +set ERROR_CODE=1 + +:end +@endlocal & set ERROR_CODE=%ERROR_CODE% + +if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost +@REM check for post script, once with legacy .bat ending and once with .cmd ending +if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat" +if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd" +:skipRcPost + +@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' +if "%MAVEN_BATCH_PAUSE%"=="on" pause + +if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE% + +cmd /C exit /B %ERROR_CODE% diff --git a/package.bat b/package.bat new file mode 100644 index 0000000..7dbf0fc --- /dev/null +++ b/package.bat @@ -0,0 +1,2 @@ +@echo off +jpackage --type app-image -n EagleEyeRegister -m com.keyware.regtool/com.keyware.regtool.Application --runtime-image target\App --temp target\temp --dest target\EagleEyeRegister --icon src\main\resources\logo.ico \ No newline at end of file diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..8787616 --- /dev/null +++ b/pom.xml @@ -0,0 +1,69 @@ + + + 4.0.0 + + com.keyware + EagleEyeRegister + 1.0-SNAPSHOT + EagleEyeRegister + + + UTF-8 + 5.9.2 + + + + + org.openjfx + javafx-controls + 17.0.6 + + + org.openjfx + javafx-fxml + 17.0.6 + + + org.openjfx + javafx-web + 17.0.6 + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.11.0 + + 11 + 11 + + + + org.openjfx + javafx-maven-plugin + 0.0.8 + + + + default-cli + + com.keyware.regtool/com.keyware.regtool.Application + App + App + App + true + true + true + + + + + + + + \ No newline at end of file diff --git a/src/main/java/cn/hutool/core/annotation/AbstractAnnotationSynthesizer.java b/src/main/java/cn/hutool/core/annotation/AbstractAnnotationSynthesizer.java new file mode 100644 index 0000000..1b04ebd --- /dev/null +++ b/src/main/java/cn/hutool/core/annotation/AbstractAnnotationSynthesizer.java @@ -0,0 +1,168 @@ +package cn.hutool.core.annotation; + +import cn.hutool.core.annotation.scanner.AnnotationScanner; +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.ObjectUtil; + +import java.lang.annotation.Annotation; +import java.util.Collection; +import java.util.Comparator; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * {@link AnnotationSynthesizer}的基本实现 + * + * @author huangchengxing + */ +public abstract class AbstractAnnotationSynthesizer implements AnnotationSynthesizer { + + /** + * 合成注解来源最初来源 + */ + protected final T source; + + /** + * 包含根注解以及其元注解在内的全部注解实例 + */ + protected final Map, SynthesizedAnnotation> synthesizedAnnotationMap; + + /** + * 已经合成过的注解对象 + */ + private final Map, Annotation> synthesizedProxyAnnotations; + + /** + * 合成注解选择器 + */ + protected final SynthesizedAnnotationSelector annotationSelector; + + /** + * 合成注解属性处理器 + */ + protected final Collection postProcessors; + + /** + * 注解扫描器 + */ + protected final AnnotationScanner annotationScanner; + + /** + * 构造一个注解合成器 + * + * @param source 当前查找的注解对象 + * @param annotationSelector 合成注解选择器 + * @param annotationPostProcessors 注解后置处理器 + * @param annotationScanner 注解扫描器,该扫描器需要支持扫描注解类 + */ + protected AbstractAnnotationSynthesizer( + T source, + SynthesizedAnnotationSelector annotationSelector, + Collection annotationPostProcessors, + AnnotationScanner annotationScanner) { + Assert.notNull(source, "source must not null"); + Assert.notNull(annotationSelector, "annotationSelector must not null"); + Assert.notNull(annotationPostProcessors, "annotationPostProcessors must not null"); + Assert.notNull(annotationPostProcessors, "annotationScanner must not null"); + + this.source = source; + this.annotationSelector = annotationSelector; + this.annotationScanner = annotationScanner; + this.postProcessors = CollUtil.unmodifiable( + CollUtil.sort(annotationPostProcessors, Comparator.comparing(SynthesizedAnnotationPostProcessor::order)) + ); + this.synthesizedProxyAnnotations = new LinkedHashMap<>(); + this.synthesizedAnnotationMap = MapUtil.unmodifiable(loadAnnotations()); + annotationPostProcessors.forEach(processor -> + synthesizedAnnotationMap.values().forEach(synthesized -> processor.process(synthesized, this)) + ); + } + + /** + * 加载合成注解的必要属性 + * + * @return 合成注解 + */ + protected abstract Map, SynthesizedAnnotation> loadAnnotations(); + + /** + * 根据指定的注解类型和对应注解对象,合成最终所需的合成注解 + * + * @param annotationType 注解类型 + * @param annotation 合成注解对象 + * @param 注解类型 + * @return 最终所需的合成注解 + */ + protected abstract A synthesize(Class annotationType, SynthesizedAnnotation annotation); + + /** + * 获取合成注解来源最初来源 + * + * @return 合成注解来源最初来源 + */ + @Override + public T getSource() { + return source; + } + + /** + * 合成注解选择器 + * + * @return 注解选择器 + */ + @Override + public SynthesizedAnnotationSelector getAnnotationSelector() { + return annotationSelector; + } + + /** + * 获取合成注解后置处理器 + * + * @return 合成注解后置处理器 + */ + @Override + public Collection getAnnotationPostProcessors() { + return postProcessors; + } + + /** + * 获取已合成的注解 + * + * @param annotationType 注解类型 + * @return 已合成的注解 + */ + @Override + public SynthesizedAnnotation getSynthesizedAnnotation(Class annotationType) { + return synthesizedAnnotationMap.get(annotationType); + } + + /** + * 获取全部的合成注解 + * + * @return 合成注解 + */ + @Override + public Map, SynthesizedAnnotation> getAllSynthesizedAnnotation() { + return synthesizedAnnotationMap; + } + + /** + * 获取合成注解 + * + * @param annotationType 注解类型 + * @param 注解类型 + * @return 类型 + */ + @SuppressWarnings("unchecked") + @Override + public A synthesize(Class annotationType) { + return (A)synthesizedProxyAnnotations.computeIfAbsent(annotationType, type -> { + final SynthesizedAnnotation synthesizedAnnotation = synthesizedAnnotationMap.get(annotationType); + return ObjectUtil.isNull(synthesizedAnnotation) ? + null : synthesize(annotationType, synthesizedAnnotation); + }); + } + +} diff --git a/src/main/java/cn/hutool/core/annotation/AbstractLinkAnnotationPostProcessor.java b/src/main/java/cn/hutool/core/annotation/AbstractLinkAnnotationPostProcessor.java new file mode 100644 index 0000000..0f97dbc --- /dev/null +++ b/src/main/java/cn/hutool/core/annotation/AbstractLinkAnnotationPostProcessor.java @@ -0,0 +1,163 @@ +package cn.hutool.core.annotation; + +import cn.hutool.core.lang.Assert; +import cn.hutool.core.lang.Opt; +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.ObjectUtil; + +import java.lang.annotation.Annotation; +import java.util.HashMap; +import java.util.Map; + +/** + * {@link SynthesizedAnnotationPostProcessor}的基本实现, + * 用于处理注解中带有{@link Link}注解的属性。 + * + * @author huangchengxing + * @see MirrorLinkAnnotationPostProcessor + * @see AliasLinkAnnotationPostProcessor + */ +public abstract class AbstractLinkAnnotationPostProcessor implements SynthesizedAnnotationPostProcessor { + + /** + * 若一个注解属性上存在{@link Link}注解,注解的{@link Link#type()}返回值在{@link #processTypes()}中存在, + * 且此{@link Link}指定的注解对象在当前的{@link SynthesizedAggregateAnnotation}中存在, + * 则从聚合器中获取类型对应的合成注解对象,与该对象中的指定属性,然后将全部关联数据交给 + * {@link #processLinkedAttribute}处理。 + * + * @param synthesizedAnnotation 合成的注解 + * @param synthesizer 合成注解聚合器 + */ + @Override + public void process(SynthesizedAnnotation synthesizedAnnotation, AnnotationSynthesizer synthesizer) { + final Map attributeMap = new HashMap<>(synthesizedAnnotation.getAttributes()); + attributeMap.forEach((originalAttributeName, originalAttribute) -> { + // 获取注解 + final Link link = getLinkAnnotation(originalAttribute, processTypes()); + if (ObjectUtil.isNull(link)) { + return; + } + // 获取注解属性 + final SynthesizedAnnotation linkedAnnotation = getLinkedAnnotation(link, synthesizer, synthesizedAnnotation.annotationType()); + if (ObjectUtil.isNull(linkedAnnotation)) { + return; + } + final AnnotationAttribute linkedAttribute = linkedAnnotation.getAttributes().get(link.attribute()); + // 处理 + processLinkedAttribute( + synthesizer, link, + synthesizedAnnotation, synthesizedAnnotation.getAttributes().get(originalAttributeName), + linkedAnnotation, linkedAttribute + ); + }); + } + + // =========================== 抽象方法 =========================== + + /** + * 当属性上存在{@link Link}注解时,仅当{@link Link#type()}在本方法返回值内存在时才进行处理 + * + * @return 支持处理的{@link RelationType}类型 + */ + protected abstract RelationType[] processTypes(); + + /** + * 对关联的合成注解对象及其关联属性的处理 + * + * @param synthesizer 注解合成器 + * @param annotation {@code originalAttribute}上的{@link Link}注解对象 + * @param originalAnnotation 当前正在处理的{@link SynthesizedAnnotation}对象 + * @param originalAttribute {@code originalAnnotation}上的待处理的属性 + * @param linkedAnnotation {@link Link}指向的关联注解对象 + * @param linkedAttribute {@link Link}指向的{@code originalAnnotation}中的关联属性,该参数可能为空 + */ + protected abstract void processLinkedAttribute( + AnnotationSynthesizer synthesizer, Link annotation, + SynthesizedAnnotation originalAnnotation, AnnotationAttribute originalAttribute, + SynthesizedAnnotation linkedAnnotation, AnnotationAttribute linkedAttribute + ); + + // =========================== @Link注解的处理 =========================== + + /** + * 从注解属性上获取指定类型的{@link Link}注解 + * + * @param attribute 注解属性 + * @param relationTypes 类型 + * @return 注解 + */ + protected Link getLinkAnnotation(AnnotationAttribute attribute, RelationType... relationTypes) { + return Opt.ofNullable(attribute) + .map(t -> AnnotationUtil.getSynthesizedAnnotation(attribute.getAttribute(), Link.class)) + .filter(a -> ArrayUtil.contains(relationTypes, a.type())) + .get(); + } + + /** + * 从合成注解中获取{@link Link#type()}指定的注解对象 + * + * @param annotation {@link Link}注解 + * @param synthesizer 注解合成器 + * @param defaultType 默认类型 + * @return {@link SynthesizedAnnotation} + */ + protected SynthesizedAnnotation getLinkedAnnotation(Link annotation, AnnotationSynthesizer synthesizer, Class defaultType) { + final Class targetAnnotationType = getLinkedAnnotationType(annotation, defaultType); + return synthesizer.getSynthesizedAnnotation(targetAnnotationType); + } + + /** + * 若{@link Link#annotation()}获取的类型{@code Annotation#getClass()},则返回{@code defaultType}, + * 否则返回{@link Link#annotation()}指定的类型 + * + * @param annotation {@link Link}注解 + * @param defaultType 默认注解类型 + * @return 注解类型 + */ + protected Class getLinkedAnnotationType(Link annotation, Class defaultType) { + return ObjectUtil.equals(annotation.annotation(), Annotation.class) ? + defaultType : annotation.annotation(); + } + + // =========================== 注解属性的校验 =========================== + + /** + * 校验两个注解属性的返回值类型是否一致 + * + * @param original 原属性 + * @param alias 别名属性 + */ + protected void checkAttributeType(AnnotationAttribute original, AnnotationAttribute alias) { + Assert.equals( + original.getAttributeType(), alias.getAttributeType(), + "return type of the linked attribute [{}] is inconsistent with the original [{}]", + original.getAttribute(), alias.getAttribute() + ); + } + + /** + * 检查{@link Link}指向的注解属性是否就是本身 + * + * @param original {@link Link}注解的属性 + * @param linked {@link Link}指向的注解属性 + */ + protected void checkLinkedSelf(AnnotationAttribute original, AnnotationAttribute linked) { + boolean linkSelf = (original == linked) || ObjectUtil.equals(original.getAttribute(), linked.getAttribute()); + Assert.isFalse(linkSelf, "cannot link self [{}]", original.getAttribute()); + } + + /** + * 检查{@link Link}指向的注解属性是否存在 + * + * @param original {@link Link}注解的属性 + * @param linkedAttribute {@link Link}指向的注解属性 + * @param annotation {@link Link}注解 + */ + protected void checkLinkedAttributeNotNull(AnnotationAttribute original, AnnotationAttribute linkedAttribute, Link annotation) { + Assert.notNull(linkedAttribute, "cannot find linked attribute [{}] of original [{}] in [{}]", + original.getAttribute(), annotation.attribute(), + getLinkedAnnotationType(annotation, original.getAnnotationType()) + ); + } + +} diff --git a/src/main/java/cn/hutool/core/annotation/AbstractWrappedAnnotationAttribute.java b/src/main/java/cn/hutool/core/annotation/AbstractWrappedAnnotationAttribute.java new file mode 100644 index 0000000..2dc13d4 --- /dev/null +++ b/src/main/java/cn/hutool/core/annotation/AbstractWrappedAnnotationAttribute.java @@ -0,0 +1,71 @@ +package cn.hutool.core.annotation; + +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.ObjectUtil; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +/** + * {@link WrappedAnnotationAttribute}的基本实现 + * + * @author huangchengxing + * @see ForceAliasedAnnotationAttribute + * @see AliasedAnnotationAttribute + * @see MirroredAnnotationAttribute + */ +public abstract class AbstractWrappedAnnotationAttribute implements WrappedAnnotationAttribute { + + protected final AnnotationAttribute original; + protected final AnnotationAttribute linked; + + protected AbstractWrappedAnnotationAttribute(AnnotationAttribute original, AnnotationAttribute linked) { + Assert.notNull(original, "target must not null"); + Assert.notNull(linked, "linked must not null"); + this.original = original; + this.linked = linked; + } + + @Override + public AnnotationAttribute getOriginal() { + return original; + } + + @Override + public AnnotationAttribute getLinked() { + return linked; + } + + @Override + public AnnotationAttribute getNonWrappedOriginal() { + AnnotationAttribute curr = null; + AnnotationAttribute next = original; + while (next != null) { + curr = next; + next = next.isWrapped() ? ((WrappedAnnotationAttribute)curr).getOriginal() : null; + } + return curr; + } + + @Override + public Collection getAllLinkedNonWrappedAttributes() { + List leafAttributes = new ArrayList<>(); + collectLeafAttribute(this, leafAttributes); + return leafAttributes; + } + + private void collectLeafAttribute(AnnotationAttribute curr, List leafAttributes) { + if (ObjectUtil.isNull(curr)) { + return; + } + if (!curr.isWrapped()) { + leafAttributes.add(curr); + return; + } + WrappedAnnotationAttribute wrappedAttribute = (WrappedAnnotationAttribute)curr; + collectLeafAttribute(wrappedAttribute.getOriginal(), leafAttributes); + collectLeafAttribute(wrappedAttribute.getLinked(), leafAttributes); + } + +} diff --git a/src/main/java/cn/hutool/core/annotation/AggregateAnnotation.java b/src/main/java/cn/hutool/core/annotation/AggregateAnnotation.java new file mode 100644 index 0000000..000c991 --- /dev/null +++ b/src/main/java/cn/hutool/core/annotation/AggregateAnnotation.java @@ -0,0 +1,27 @@ +package cn.hutool.core.annotation; + +import java.lang.annotation.Annotation; + +/** + * 表示一组被聚合在一起的注解对象 + * + * @author huangchengxing + */ +public interface AggregateAnnotation extends Annotation { + + /** + * 在聚合中是否存在的指定类型注解对象 + * + * @param annotationType 注解类型 + * @return 是否 + */ + boolean isAnnotationPresent(Class annotationType); + + /** + * 获取聚合中的全部注解对象 + * + * @return 注解对象 + */ + Annotation[] getAnnotations(); + +} diff --git a/src/main/java/cn/hutool/core/annotation/Alias.java b/src/main/java/cn/hutool/core/annotation/Alias.java new file mode 100644 index 0000000..3c1274e --- /dev/null +++ b/src/main/java/cn/hutool/core/annotation/Alias.java @@ -0,0 +1,26 @@ +package cn.hutool.core.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * 别名注解,使用此注解的字段、方法、参数等会有一个别名,用于Bean拷贝、Bean转Map等 + * + * @author Looly + * @since 5.1.1 + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER}) +public @interface Alias { + + /** + * 别名值,即使用此注解要替换成的别名名称 + * + * @return 别名值 + */ + String value(); +} diff --git a/src/main/java/cn/hutool/core/annotation/AliasAnnotationPostProcessor.java b/src/main/java/cn/hutool/core/annotation/AliasAnnotationPostProcessor.java new file mode 100644 index 0000000..ac5481a --- /dev/null +++ b/src/main/java/cn/hutool/core/annotation/AliasAnnotationPostProcessor.java @@ -0,0 +1,66 @@ +package cn.hutool.core.annotation; + +import cn.hutool.core.lang.Assert; +import cn.hutool.core.lang.Opt; +import cn.hutool.core.map.ForestMap; +import cn.hutool.core.map.LinkedForestMap; +import cn.hutool.core.map.TreeEntry; +import cn.hutool.core.util.ClassUtil; +import cn.hutool.core.util.ObjectUtil; + +import java.util.Map; + +/** + *

用于处理注解对象中带有{@link Alias}注解的属性。
+ * 当该处理器执行完毕后,{@link Alias}注解指向的目标注解的属性将会被包装并替换为 + * {@link ForceAliasedAnnotationAttribute}。 + * + * @author huangchengxing + * @see Alias + * @see ForceAliasedAnnotationAttribute + */ +public class AliasAnnotationPostProcessor implements SynthesizedAnnotationPostProcessor { + + @Override + public int order() { + return Integer.MIN_VALUE; + } + + @Override + public void process(SynthesizedAnnotation synthesizedAnnotation, AnnotationSynthesizer synthesizer) { + final Map attributeMap = synthesizedAnnotation.getAttributes(); + + // 记录别名与属性的关系 + final ForestMap attributeAliasMappings = new LinkedForestMap<>(false); + attributeMap.forEach((attributeName, attribute) -> { + final String alias = Opt.ofNullable(attribute.getAnnotation(Alias.class)) + .map(Alias::value) + .orElse(null); + if (ObjectUtil.isNull(alias)) { + return; + } + final AnnotationAttribute aliasAttribute = attributeMap.get(alias); + Assert.notNull(aliasAttribute, "no method for alias: [{}]", alias); + attributeAliasMappings.putLinkedNodes(alias, aliasAttribute, attributeName, attribute); + }); + + // 处理别名 + attributeMap.forEach((attributeName, attribute) -> { + final AnnotationAttribute resolvedAttribute = Opt.ofNullable(attributeName) + .map(attributeAliasMappings::getRootNode) + .map(TreeEntry::getValue) + .orElse(attribute); + Assert.isTrue( + ObjectUtil.isNull(resolvedAttribute) + || ClassUtil.isAssignable(attribute.getAttributeType(), resolvedAttribute.getAttributeType()), + "return type of the root alias method [{}] is inconsistent with the original [{}]", + resolvedAttribute.getClass(), attribute.getAttributeType() + ); + if (attribute != resolvedAttribute) { + attributeMap.put(attributeName, new ForceAliasedAnnotationAttribute(attribute, resolvedAttribute)); + } + }); + synthesizedAnnotation.setAttributes(attributeMap); + } + +} diff --git a/src/main/java/cn/hutool/core/annotation/AliasFor.java b/src/main/java/cn/hutool/core/annotation/AliasFor.java new file mode 100644 index 0000000..bf16323 --- /dev/null +++ b/src/main/java/cn/hutool/core/annotation/AliasFor.java @@ -0,0 +1,39 @@ +package cn.hutool.core.annotation; + +import java.lang.annotation.*; + +/** + *

{@link Link}的子注解。表示“原始属性”将作为“关联属性”的别名。 + *

    + *
  • 当“原始属性”为默认值时,获取“关联属性”将返回“关联属性”本身的值;
  • + *
  • 当“原始属性”不为默认值时,获取“关联属性”将返回“原始属性”的值;
  • + *
+ * 注意,该注解与{@link Link}、{@link ForceAliasFor}或{@link MirrorFor}一起使用时,将只有被声明在最上面的注解会生效 + * + * @author huangchengxing + * @see Link + * @see RelationType#ALIAS_FOR + */ +@Link(type = RelationType.ALIAS_FOR) +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE}) +public @interface AliasFor { + + /** + * 产生关联的注解类型,当不指定时,默认指注释的属性所在的类 + * + * @return 注解类型 + */ + @Link(annotation = Link.class, attribute = "annotation", type = RelationType.FORCE_ALIAS_FOR) + Class annotation() default Annotation.class; + + /** + * {@link #annotation()}指定注解中关联的属性 + * + * @return 关联属性 + */ + @Link(annotation = Link.class, attribute = "attribute", type = RelationType.FORCE_ALIAS_FOR) + String attribute() default ""; + +} diff --git a/src/main/java/cn/hutool/core/annotation/AliasLinkAnnotationPostProcessor.java b/src/main/java/cn/hutool/core/annotation/AliasLinkAnnotationPostProcessor.java new file mode 100644 index 0000000..0cf5b22 --- /dev/null +++ b/src/main/java/cn/hutool/core/annotation/AliasLinkAnnotationPostProcessor.java @@ -0,0 +1,126 @@ +package cn.hutool.core.annotation; + +import cn.hutool.core.lang.Assert; +import cn.hutool.core.lang.Opt; +import cn.hutool.core.util.ObjectUtil; + +import java.util.function.BinaryOperator; + +/** + *

用于处理注解对象中带有{@link Link}注解,且{@link Link#type()}为 + * {@link RelationType#ALIAS_FOR}或{@link RelationType#FORCE_ALIAS_FOR}的属性。
+ * 当该处理器执行完毕后,{@link Link}注解指向的目标注解的属性将会被包装并替换为 + * {@link AliasedAnnotationAttribute}或{@link ForceAliasedAnnotationAttribute}。 + * + * @author huangchengxing + * @see RelationType#ALIAS_FOR + * @see AliasedAnnotationAttribute + * @see RelationType#FORCE_ALIAS_FOR + * @see ForceAliasedAnnotationAttribute + */ +public class AliasLinkAnnotationPostProcessor extends AbstractLinkAnnotationPostProcessor { + + private static final RelationType[] PROCESSED_RELATION_TYPES = new RelationType[]{ RelationType.ALIAS_FOR, RelationType.FORCE_ALIAS_FOR }; + + @Override + public int order() { + return Integer.MIN_VALUE + 2; + } + + /** + * 该处理器只处理{@link Link#type()}类型为{@link RelationType#ALIAS_FOR}和{@link RelationType#FORCE_ALIAS_FOR}的注解属性 + * + * @return 含有{@link RelationType#ALIAS_FOR}和{@link RelationType#FORCE_ALIAS_FOR}的数组 + */ + @Override + protected RelationType[] processTypes() { + return PROCESSED_RELATION_TYPES; + } + + /** + * 获取{@link Link}指向的目标注解属性,并根据{@link Link#type()}的类型是 + * {@link RelationType#ALIAS_FOR}或{@link RelationType#FORCE_ALIAS_FOR} + * 将目标注解属性包装为{@link AliasedAnnotationAttribute}或{@link ForceAliasedAnnotationAttribute}, + * 然后用包装后注解属性在对应的合成注解中替换原始的目标注解属性 + * + * @param synthesizer 注解合成器 + * @param annotation {@code originalAttribute}上的{@link Link}注解对象 + * @param originalAnnotation 当前正在处理的{@link SynthesizedAnnotation}对象 + * @param originalAttribute {@code originalAnnotation}上的待处理的属性 + * @param linkedAnnotation {@link Link}指向的关联注解对象 + * @param linkedAttribute {@link Link}指向的{@code originalAnnotation}中的关联属性,该参数可能为空 + */ + @Override + protected void processLinkedAttribute( + AnnotationSynthesizer synthesizer, Link annotation, + SynthesizedAnnotation originalAnnotation, AnnotationAttribute originalAttribute, + SynthesizedAnnotation linkedAnnotation, AnnotationAttribute linkedAttribute) { + // 校验别名关系 + checkAliasRelation(annotation, originalAttribute, linkedAttribute); + // 处理aliasFor类型的关系 + if (RelationType.ALIAS_FOR.equals(annotation.type())) { + wrappingLinkedAttribute(synthesizer, originalAttribute, linkedAttribute, AliasedAnnotationAttribute::new); + return; + } + // 处理forceAliasFor类型的关系 + wrappingLinkedAttribute(synthesizer, originalAttribute, linkedAttribute, ForceAliasedAnnotationAttribute::new); + } + + /** + * 对指定注解属性进行包装,若该属性已被包装过,则递归以其为根节点的树结构,对树上全部的叶子节点进行包装 + */ + private void wrappingLinkedAttribute( + AnnotationSynthesizer synthesizer, AnnotationAttribute originalAttribute, AnnotationAttribute aliasAttribute, BinaryOperator wrapping) { + // 不是包装属性 + if (!aliasAttribute.isWrapped()) { + processAttribute(synthesizer, originalAttribute, aliasAttribute, wrapping); + return; + } + // 是包装属性 + final AbstractWrappedAnnotationAttribute wrapper = (AbstractWrappedAnnotationAttribute)aliasAttribute; + wrapper.getAllLinkedNonWrappedAttributes().forEach( + t -> processAttribute(synthesizer, originalAttribute, t, wrapping) + ); + } + + /** + * 获取指定注解属性,然后将其再进行一层包装 + */ + private void processAttribute( + AnnotationSynthesizer synthesizer, AnnotationAttribute originalAttribute, + AnnotationAttribute target, BinaryOperator wrapping) { + Opt.ofNullable(target.getAnnotationType()) + .map(synthesizer::getSynthesizedAnnotation) + .ifPresent(t -> t.replaceAttribute(target.getAttributeName(), old -> wrapping.apply(old, originalAttribute))); + } + + /** + * 基本校验 + */ + private void checkAliasRelation(Link annotation, AnnotationAttribute originalAttribute, AnnotationAttribute linkedAttribute) { + checkLinkedAttributeNotNull(originalAttribute, linkedAttribute, annotation); + checkAttributeType(originalAttribute, linkedAttribute); + checkCircularDependency(originalAttribute, linkedAttribute); + } + + /** + * 检查两个属性是否互为别名 + */ + private void checkCircularDependency(AnnotationAttribute original, AnnotationAttribute alias) { + checkLinkedSelf(original, alias); + Link annotation = getLinkAnnotation(alias, RelationType.ALIAS_FOR, RelationType.FORCE_ALIAS_FOR); + if (ObjectUtil.isNull(annotation)) { + return; + } + final Class aliasAnnotationType = getLinkedAnnotationType(annotation, alias.getAnnotationType()); + if (ObjectUtil.notEqual(aliasAnnotationType, original.getAnnotationType())) { + return; + } + Assert.notEquals( + annotation.attribute(), original.getAttributeName(), + "circular reference between the alias attribute [{}] and the original attribute [{}]", + alias.getAttribute(), original.getAttribute() + ); + } + +} diff --git a/src/main/java/cn/hutool/core/annotation/AliasedAnnotationAttribute.java b/src/main/java/cn/hutool/core/annotation/AliasedAnnotationAttribute.java new file mode 100644 index 0000000..1c78d81 --- /dev/null +++ b/src/main/java/cn/hutool/core/annotation/AliasedAnnotationAttribute.java @@ -0,0 +1,36 @@ +package cn.hutool.core.annotation; + +/** + *

表示一个具有别名的属性。 + * 当别名属性值为默认值时,优先返回原属性的值,当别名属性不为默认值时,优先返回别名属性的值 + * + * @author huangchengxing + * @see AliasLinkAnnotationPostProcessor + * @see RelationType#ALIAS_FOR + */ +public class AliasedAnnotationAttribute extends AbstractWrappedAnnotationAttribute { + + protected AliasedAnnotationAttribute(AnnotationAttribute origin, AnnotationAttribute linked) { + super(origin, linked); + } + + /** + * 若{@link #linked}为默认值,则返回{@link #original}的值,否则返回{@link #linked}的值 + * + * @return 属性值 + */ + @Override + public Object getValue() { + return linked.isValueEquivalentToDefaultValue() ? super.getValue() : linked.getValue(); + } + + /** + * 当{@link #original}与{@link #linked}都为默认值时返回{@code true} + * + * @return 是否 + */ + @Override + public boolean isValueEquivalentToDefaultValue() { + return linked.isValueEquivalentToDefaultValue() && original.isValueEquivalentToDefaultValue(); + } +} diff --git a/src/main/java/cn/hutool/core/annotation/AnnotationAttribute.java b/src/main/java/cn/hutool/core/annotation/AnnotationAttribute.java new file mode 100644 index 0000000..f3d976d --- /dev/null +++ b/src/main/java/cn/hutool/core/annotation/AnnotationAttribute.java @@ -0,0 +1,104 @@ +package cn.hutool.core.annotation; + +import cn.hutool.core.util.ReflectUtil; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; + +/** + *

表示注解的某个属性,等同于绑定的调用对象的{@link Method}方法。
+ * 在{@link SynthesizedAggregateAnnotation}的解析以及取值过程中, + * 可以通过设置{@link SynthesizedAnnotation}的注解属性, + * 从而使得可以从一个注解对象中属性获取另一个注解对象的属性值 + * + *

一般情况下,注解属性的处理会发生在{@link SynthesizedAnnotationPostProcessor}调用时 + * + * @author huangchengxing + * @see SynthesizedAnnotationPostProcessor + * @see WrappedAnnotationAttribute + * @see CacheableAnnotationAttribute + * @see AbstractWrappedAnnotationAttribute + * @see ForceAliasedAnnotationAttribute + * @see AliasedAnnotationAttribute + * @see MirroredAnnotationAttribute + */ +public interface AnnotationAttribute { + + /** + * 获取注解对象 + * + * @return 注解对象 + */ + Annotation getAnnotation(); + + /** + * 获取注解属性对应的方法 + * + * @return 注解属性对应的方法 + */ + Method getAttribute(); + + /** + * 获取声明属性的注解类 + * + * @return 声明注解的注解类 + */ + default Class getAnnotationType() { + return getAttribute().getDeclaringClass(); + } + + /** + * 获取属性名称 + * + * @return 属性名称 + */ + default String getAttributeName() { + return getAttribute().getName(); + } + + /** + * 获取注解属性 + * + * @return 注解属性 + */ + default Object getValue() { + return ReflectUtil.invoke(getAnnotation(), getAttribute()); + } + + /** + * 该注解属性的值是否等于默认值 + * + * @return 该注解属性的值是否等于默认值 + */ + boolean isValueEquivalentToDefaultValue(); + + /** + * 获取属性类型 + * + * @return 属性类型 + */ + default Class getAttributeType() { + return getAttribute().getReturnType(); + } + + /** + * 获取属性上的注解 + * + * @param 注解类型 + * @param annotationType 注解类型 + * @return 注解对象 + */ + default T getAnnotation(Class annotationType) { + return getAttribute().getAnnotation(annotationType); + } + + /** + * 当前注解属性是否已经被{@link WrappedAnnotationAttribute}包装 + * + * @return boolean + */ + default boolean isWrapped() { + return false; + } + +} diff --git a/src/main/java/cn/hutool/core/annotation/AnnotationAttributeValueProvider.java b/src/main/java/cn/hutool/core/annotation/AnnotationAttributeValueProvider.java new file mode 100644 index 0000000..b127d75 --- /dev/null +++ b/src/main/java/cn/hutool/core/annotation/AnnotationAttributeValueProvider.java @@ -0,0 +1,18 @@ +package cn.hutool.core.annotation; + +/** + * 表示一个可以从当前接口的实现类中,获得特定的属性值 + */ +@FunctionalInterface +public interface AnnotationAttributeValueProvider { + + /** + * 获取注解属性值 + * + * @param attributeName 属性名称 + * @param attributeType 属性类型 + * @return 注解属性值 + */ + Object getAttributeValue(String attributeName, Class attributeType); + +} diff --git a/src/main/java/cn/hutool/core/annotation/AnnotationProxy.java b/src/main/java/cn/hutool/core/annotation/AnnotationProxy.java new file mode 100644 index 0000000..0c4fdfe --- /dev/null +++ b/src/main/java/cn/hutool/core/annotation/AnnotationProxy.java @@ -0,0 +1,88 @@ +package cn.hutool.core.annotation; + +import cn.hutool.core.util.ReflectUtil; +import cn.hutool.core.util.StrUtil; + +import java.io.Serializable; +import java.lang.annotation.Annotation; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.util.HashMap; +import java.util.Map; + +/** + * 注解代理
+ * 通过代理指定注解,可以自定义调用注解的方法逻辑,如支持{@link Alias} 注解 + * + * @param 注解类型 + * @since 5.7.23 + */ +public class AnnotationProxy implements Annotation, InvocationHandler, Serializable { + private static final long serialVersionUID = 1L; + + private final T annotation; + private final Class type; + private final Map attributes; + + /** + * 构造 + * + * @param annotation 注解 + */ + public AnnotationProxy(T annotation) { + this.annotation = annotation; + //noinspection unchecked + this.type = (Class) annotation.annotationType(); + this.attributes = initAttributes(); + } + + + @Override + public Class annotationType() { + return type; + } + + @Override + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + + // 注解别名 + Alias alias = method.getAnnotation(Alias.class); + if(null != alias){ + final String name = alias.value(); + if(StrUtil.isNotBlank(name)){ + if(!attributes.containsKey(name)){ + throw new IllegalArgumentException(StrUtil.format("No method for alias: [{}]", name)); + } + return attributes.get(name); + } + } + + final Object value = attributes.get(method.getName()); + if (value != null) { + return value; + } + return method.invoke(this, args); + } + + /** + * 初始化注解的属性
+ * 此方法预先调用所有注解的方法,将注解方法值缓存于attributes中 + * + * @return 属性(方法结果)映射 + */ + private Map initAttributes() { + final Method[] methods = ReflectUtil.getMethods(this.type); + final Map attributes = new HashMap<>(methods.length, 1); + + for (Method method : methods) { + // 跳过匿名内部类自动生成的方法 + if (method.isSynthetic()) { + continue; + } + + attributes.put(method.getName(), ReflectUtil.invoke(this.annotation, method)); + } + + return attributes; + } +} diff --git a/src/main/java/cn/hutool/core/annotation/AnnotationSynthesizer.java b/src/main/java/cn/hutool/core/annotation/AnnotationSynthesizer.java new file mode 100644 index 0000000..298a33d --- /dev/null +++ b/src/main/java/cn/hutool/core/annotation/AnnotationSynthesizer.java @@ -0,0 +1,77 @@ +package cn.hutool.core.annotation; + +import java.lang.annotation.Annotation; +import java.util.Collection; +import java.util.Map; + +/** + *

注解合成器,用于处理一组给定的与{@link #getSource()}具有直接或间接联系的注解对象, + * 并返回与原始注解对象具有不同属性的“合成”注解。 + * + *

合成注解一般被用于处理类层级结果中具有直接或间接关联的注解对象, + * 当实例被创建时,会获取到这些注解对象,并使用{@link SynthesizedAnnotationSelector}对类型相同的注解进行过滤, + * 并最终得到类型不重复的有效注解对象。这些有效注解将被包装为{@link SynthesizedAnnotation}, + * 然后最终用于“合成”一个{@link SynthesizedAggregateAnnotation}。
+ * {@link SynthesizedAnnotationSelector}是合成注解生命周期中的第一个钩子, + * 自定义选择器以拦截原始注解被扫描的过程。 + * + *

当合成注解完成对待合成注解的扫描,并完成了必要属性的加载后, + * 将会按顺序依次调用{@link SynthesizedAnnotationPostProcessor}, + * 注解后置处理器允许用于对完成注解的待合成注解进行二次调整, + * 该钩子一般用于根据{@link Link}注解对属性进行调整。
+ * {@link SynthesizedAnnotationPostProcessor}是合成注解生命周期中的第二个钩子, + * 自定义后置处理器以拦截原始在转为待合成注解后的初始化过程。 + * + *

使用{@link #synthesize(Class)}用于获取“合成”后的注解, + * 该注解对象的属性可能会与原始的对象属性不同。 + * + * @author huangchengxing + */ +public interface AnnotationSynthesizer { + + /** + * 获取合成注解来源最初来源 + * + * @return 合成注解来源最初来源 + */ + Object getSource(); + + /** + * 合成注解选择器 + * + * @return 注解选择器 + */ + SynthesizedAnnotationSelector getAnnotationSelector(); + + /** + * 获取合成注解后置处理器 + * + * @return 合成注解后置处理器 + */ + Collection getAnnotationPostProcessors(); + + /** + * 获取已合成的注解 + * + * @param annotationType 注解类型 + * @return 已合成的注解 + */ + SynthesizedAnnotation getSynthesizedAnnotation(Class annotationType); + + /** + * 获取全部的合成注解 + * + * @return 合成注解 + */ + Map, SynthesizedAnnotation> getAllSynthesizedAnnotation(); + + /** + * 获取合成注解 + * + * @param annotationType 注解类型 + * @param 注解类型 + * @return 类型 + */ + T synthesize(Class annotationType); + +} diff --git a/src/main/java/cn/hutool/core/annotation/AnnotationUtil.java b/src/main/java/cn/hutool/core/annotation/AnnotationUtil.java new file mode 100644 index 0000000..3509763 --- /dev/null +++ b/src/main/java/cn/hutool/core/annotation/AnnotationUtil.java @@ -0,0 +1,576 @@ +package cn.hutool.core.annotation; + +import cn.hutool.core.annotation.scanner.AnnotationScanner; +import cn.hutool.core.annotation.scanner.MetaAnnotationScanner; +import cn.hutool.core.annotation.scanner.MethodAnnotationScanner; +import cn.hutool.core.annotation.scanner.TypeAnnotationScanner; +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.exceptions.UtilException; +import cn.hutool.core.lang.Opt; +import cn.hutool.core.lang.func.Func1; +import cn.hutool.core.lang.func.LambdaUtil; +import cn.hutool.core.util.*; + +import java.lang.annotation.*; +import java.lang.invoke.SerializedLambda; +import java.lang.reflect.AnnotatedElement; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.util.*; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +/** + * 注解工具类
+ * 快速获取注解对象、注解值等工具封装 + * + * @author looly + * @since 4.0.9 + */ +public class AnnotationUtil { + + /** + * 元注解 + */ + static final Set> META_ANNOTATIONS = CollUtil.newHashSet(Target.class, // + Retention.class, // + Inherited.class, // + Documented.class, // + SuppressWarnings.class, // + Override.class, // + Deprecated.class// + ); + + /** + * 是否为Jdk自带的元注解。
+ * 包括: + *

    + *
  • {@link Target}
  • + *
  • {@link Retention}
  • + *
  • {@link Inherited}
  • + *
  • {@link Documented}
  • + *
  • {@link SuppressWarnings}
  • + *
  • {@link Override}
  • + *
  • {@link Deprecated}
  • + *
+ * + * @param annotationType 注解类型 + * @return 是否为Jdk自带的元注解 + */ + public static boolean isJdkMetaAnnotation(Class annotationType) { + return META_ANNOTATIONS.contains(annotationType); + } + + /** + * 是否不为Jdk自带的元注解。
+ * 包括: + *
    + *
  • {@link Target}
  • + *
  • {@link Retention}
  • + *
  • {@link Inherited}
  • + *
  • {@link Documented}
  • + *
  • {@link SuppressWarnings}
  • + *
  • {@link Override}
  • + *
  • {@link Deprecated}
  • + *
+ * + * @param annotationType 注解类型 + * @return 是否为Jdk自带的元注解 + */ + public static boolean isNotJdkMateAnnotation(Class annotationType) { + return !isJdkMetaAnnotation(annotationType); + } + + /** + * 将指定的被注解的元素转换为组合注解元素 + * + * @param annotationEle 注解元素 + * @return 组合注解元素 + */ + public static CombinationAnnotationElement toCombination(AnnotatedElement annotationEle) { + if (annotationEle instanceof CombinationAnnotationElement) { + return (CombinationAnnotationElement) annotationEle; + } + return new CombinationAnnotationElement(annotationEle); + } + + /** + * 获取指定注解 + * + * @param annotationEle {@link AnnotatedElement},可以是Class、Method、Field、Constructor、ReflectPermission + * @param isToCombination 是否为转换为组合注解,组合注解可以递归获取注解的注解 + * @return 注解对象 + */ + public static Annotation[] getAnnotations(AnnotatedElement annotationEle, boolean isToCombination) { + return getAnnotations(annotationEle, isToCombination, (Predicate) null); + } + + /** + * 获取组合注解 + * + * @param 注解类型 + * @param annotationEle {@link AnnotatedElement},可以是Class、Method、Field、Constructor、ReflectPermission + * @param annotationType 限定的 + * @return 注解对象数组 + * @since 5.8.0 + */ + public static T[] getCombinationAnnotations(AnnotatedElement annotationEle, Class annotationType) { + return getAnnotations(annotationEle, true, annotationType); + } + + /** + * 获取指定注解 + * + * @param 注解类型 + * @param annotationEle {@link AnnotatedElement},可以是Class、Method、Field、Constructor、ReflectPermission + * @param isToCombination 是否为转换为组合注解,组合注解可以递归获取注解的注解 + * @param annotationType 限定的 + * @return 注解对象数组 + * @since 5.8.0 + */ + public static T[] getAnnotations(AnnotatedElement annotationEle, boolean isToCombination, Class annotationType) { + final Annotation[] annotations = getAnnotations(annotationEle, isToCombination, + (annotation -> null == annotationType || annotationType.isAssignableFrom(annotation.getClass()))); + + final T[] result = ArrayUtil.newArray(annotationType, annotations.length); + for (int i = 0; i < annotations.length; i++) { + //noinspection unchecked + result[i] = (T) annotations[i]; + } + return result; + } + + /** + * 获取指定注解 + * + * @param annotationEle {@link AnnotatedElement},可以是Class、Method、Field、Constructor、ReflectPermission + * @param isToCombination 是否为转换为组合注解,组合注解可以递归获取注解的注解 + * @param predicate 过滤器,{@link Predicate#test(Object)}返回{@code true}保留,否则不保留 + * @return 注解对象,如果提供的{@link AnnotatedElement}为{@code null},返回{@code null} + * @since 5.8.0 + */ + public static Annotation[] getAnnotations(AnnotatedElement annotationEle, boolean isToCombination, Predicate predicate) { + if (null == annotationEle) { + return null; + } + + if (isToCombination) { + if (null == predicate) { + return toCombination(annotationEle).getAnnotations(); + } + return CombinationAnnotationElement.of(annotationEle, predicate).getAnnotations(); + } + + final Annotation[] result = annotationEle.getAnnotations(); + if (null == predicate) { + return result; + } + return ArrayUtil.filter(result, predicate::test); + } + + /** + * 获取指定注解 + * + * @param
注解类型 + * @param annotationEle {@link AnnotatedElement},可以是Class、Method、Field、Constructor、ReflectPermission + * @param annotationType 注解类型 + * @return 注解对象 + */ + public static A getAnnotation(AnnotatedElement annotationEle, Class annotationType) { + return (null == annotationEle) ? null : toCombination(annotationEle).getAnnotation(annotationType); + } + + /** + * 检查是否包含指定注解指定注解 + * + * @param annotationEle {@link AnnotatedElement},可以是Class、Method、Field、Constructor、ReflectPermission + * @param annotationType 注解类型 + * @return 是否包含指定注解 + * @since 5.4.2 + */ + public static boolean hasAnnotation(AnnotatedElement annotationEle, Class annotationType) { + return null != getAnnotation(annotationEle, annotationType); + } + + /** + * 获取指定注解默认值
+ * 如果无指定的属性方法返回null + * + * @param 注解值类型 + * @param annotationEle {@link AnnotatedElement},可以是Class、Method、Field、Constructor、ReflectPermission + * @param annotationType 注解类型 + * @return 注解对象 + * @throws UtilException 调用注解中的方法时执行异常 + */ + public static T getAnnotationValue(AnnotatedElement annotationEle, Class annotationType) throws UtilException { + return getAnnotationValue(annotationEle, annotationType, "value"); + } + + /** + * 获取指定注解属性的值
+ * 如果无指定的属性方法返回null + * + * @param 注解值类型 + * @param annotationEle {@link AnnotatedElement},可以是Class、Method、Field、Constructor、ReflectPermission + * @param annotationType 注解类型 + * @param propertyName 属性名,例如注解中定义了name()方法,则 此处传入name + * @return 注解对象 + * @throws UtilException 调用注解中的方法时执行异常 + */ + public static T getAnnotationValue(AnnotatedElement annotationEle, Class annotationType, String propertyName) throws UtilException { + final Annotation annotation = getAnnotation(annotationEle, annotationType); + if (null == annotation) { + return null; + } + + final Method method = ReflectUtil.getMethodOfObj(annotation, propertyName); + if (null == method) { + return null; + } + return ReflectUtil.invoke(annotation, method); + } + + /** + * 获取指定注解属性的值
+ * 如果无指定的属性方法返回null + * + * @param
注解类型 + * @param 注解类型值 + * @param annotationEle {@link AnnotatedElement},可以是Class、Method、Field、Constructor、ReflectPermission + * @param propertyName 属性名,例如注解中定义了name()方法,则 此处传入name + * @return 注解对象 + * @throws UtilException 调用注解中的方法时执行异常 + * @since 5.8.9 + */ + public static R getAnnotationValue(AnnotatedElement annotationEle, Func1 propertyName) { + if (propertyName == null) { + return null; + } else { + final SerializedLambda lambda = LambdaUtil.resolve(propertyName); + final String instantiatedMethodType = lambda.getInstantiatedMethodType(); + final Class annotationClass = ClassUtil.loadClass(StrUtil.sub(instantiatedMethodType, 2, StrUtil.indexOf(instantiatedMethodType, ';'))); + return getAnnotationValue(annotationEle, annotationClass, lambda.getImplMethodName()); + } + } + + /** + * 获取指定注解中所有属性值
+ * 如果无指定的属性方法返回null + * + * @param annotationEle {@link AnnotatedElement},可以是Class、Method、Field、Constructor、ReflectPermission + * @param annotationType 注解类型 + * @return 注解对象 + * @throws UtilException 调用注解中的方法时执行异常 + */ + public static Map getAnnotationValueMap(AnnotatedElement annotationEle, Class annotationType) throws UtilException { + final Annotation annotation = getAnnotation(annotationEle, annotationType); + if (null == annotation) { + return null; + } + + final Method[] methods = ReflectUtil.getMethods(annotationType, t -> { + if (ArrayUtil.isEmpty(t.getParameterTypes())) { + // 只读取无参方法 + final String name = t.getName(); + // 跳过自有的几个方法 + return (!"hashCode".equals(name)) // + && (!"toString".equals(name)) // + && (!"annotationType".equals(name)); + } + return false; + }); + + final HashMap result = new HashMap<>(methods.length, 1); + for (Method method : methods) { + result.put(method.getName(), ReflectUtil.invoke(annotation, method)); + } + return result; + } + + /** + * 获取注解类的保留时间,可选值 SOURCE(源码时),CLASS(编译时),RUNTIME(运行时),默认为 CLASS + * + * @param annotationType 注解类 + * @return 保留时间枚举 + */ + public static RetentionPolicy getRetentionPolicy(Class annotationType) { + final Retention retention = annotationType.getAnnotation(Retention.class); + if (null == retention) { + return RetentionPolicy.CLASS; + } + return retention.value(); + } + + /** + * 获取注解类可以用来修饰哪些程序元素,如 TYPE, METHOD, CONSTRUCTOR, FIELD, PARAMETER 等 + * + * @param annotationType 注解类 + * @return 注解修饰的程序元素数组 + */ + public static ElementType[] getTargetType(Class annotationType) { + final Target target = annotationType.getAnnotation(Target.class); + if (null == target) { + return new ElementType[]{ElementType.TYPE, // + ElementType.FIELD, // + ElementType.METHOD, // + ElementType.PARAMETER, // + ElementType.CONSTRUCTOR, // + ElementType.LOCAL_VARIABLE, // + ElementType.ANNOTATION_TYPE, // + ElementType.PACKAGE// + }; + } + return target.value(); + } + + /** + * 是否会保存到 Javadoc 文档中 + * + * @param annotationType 注解类 + * @return 是否会保存到 Javadoc 文档中 + */ + public static boolean isDocumented(Class annotationType) { + return annotationType.isAnnotationPresent(Documented.class); + } + + /** + * 是否可以被继承,默认为 false + * + * @param annotationType 注解类 + * @return 是否会保存到 Javadoc 文档中 + */ + public static boolean isInherited(Class annotationType) { + return annotationType.isAnnotationPresent(Inherited.class); + } + + /** + * 扫描注解类,以及注解类的{@link Class}层级结构中的注解,将返回除了{@link #META_ANNOTATIONS}中指定的JDK默认注解外, + * 按元注解对象与{@code annotationType}的距离和{@link Class#getAnnotations()}顺序排序的注解对象集合 + * + *

比如:
+ * 若{@code annotationType}为 A,且A存在元注解B,B又存在元注解C和D,则有: + *

+	 *                              |-> C.class [@a, @b]
+	 *     A.class -> B.class [@a] -|
+	 *                              |-> D.class [@a, @c]
+	 * 
+ * 扫描A,则该方法最终将返回 {@code [@a, @a, @b, @a, @c]} + * + * @param annotationType 注解类 + * @return 注解对象集合 + * @see MetaAnnotationScanner + */ + public static List scanMetaAnnotation(Class annotationType) { + return AnnotationScanner.DIRECTLY_AND_META_ANNOTATION.getAnnotationsIfSupport(annotationType); + } + + /** + *

扫描类以及类的{@link Class}层级结构中的注解,将返回除了{@link #META_ANNOTATIONS}中指定的JDK默认元注解外, + * 全部类/接口的{@link Class#getAnnotations()}方法返回的注解对象。
+ * 层级结构将按广度优先递归,遵循规则如下: + *

    + *
  • 同一层级中,优先处理父类,然后再处理父接口;
  • + *
  • 同一个接口在不同层级出现,优先选择层级距离{@code targetClass}更近的接口;
  • + *
  • 同一个接口在相同层级出现,优先选择其子类/子接口被先解析的那个;
  • + *
+ * 注解根据其声明类/接口被扫描的顺序排序,若注解都在同一个{@link Class}中被声明,则还会遵循{@link Class#getAnnotations()}的顺序。 + * + *

比如:
+ * 若{@code targetClass}为{@code A.class},且{@code A.class}存在父类{@code B.class}、父接口{@code C.class}, + * 三个类的注解声明情况如下: + *

+	 *                   |-> B.class [@a, @b]
+	 *     A.class [@a] -|
+	 *                   |-> C.class [@a, @c]
+	 * 
+ * 则该方法最终将返回 {@code [@a, @a, @b, @a, @c]} + * + * @param targetClass 类 + * @return 注解对象集合 + * @see TypeAnnotationScanner + */ + public static List scanClass(Class targetClass) { + return AnnotationScanner.TYPE_HIERARCHY.getAnnotationsIfSupport(targetClass); + } + + /** + *

扫描方法,以及该方法所在类的{@link Class}层级结构中的具有相同方法签名的方法, + * 将返回除了{@link #META_ANNOTATIONS}中指定的JDK默认元注解外, + * 全部匹配方法上{@link Method#getAnnotations()}方法返回的注解对象。
+ * 方法所在类的层级结构将按广度优先递归,遵循规则如下: + *

    + *
  • 同一层级中,优先处理父类,然后再处理父接口;
  • + *
  • 同一个接口在不同层级出现,优先选择层级距离{@code targetClass}更近的接口;
  • + *
  • 同一个接口在相同层级出现,优先选择其子类/子接口被先解析的那个;
  • + *
+ * 方法上的注解根据方法的声明类/接口被扫描的顺序排序,若注解都在同一个类的同一个方法中被声明,则还会遵循{@link Method#getAnnotations()}的顺序。 + * + *

比如:
+ * 若方法X声明于{@code A.class},且重载/重写自父类{@code B.class},并且父类中的方法X由重写至其实现的接口{@code C.class}, + * 三个类的注解声明情况如下: + *

+	 *     A#X()[@a] -> B#X()[@b] -> C#X()[@c]
+	 * 
+ * 则该方法最终将返回 {@code [@a, @b, @c]} + * + * @param method 方法 + * @return 注解对象集合 + * @see MethodAnnotationScanner + */ + public static List scanMethod(Method method) { + return AnnotationScanner.TYPE_HIERARCHY.getAnnotationsIfSupport(method); + } + + /** + * 设置新的注解的属性(字段)值 + * + * @param annotation 注解对象 + * @param annotationField 注解属性(字段)名称 + * @param value 要更新的属性值 + * @since 5.5.2 + */ + @SuppressWarnings({"rawtypes", "unchecked"}) + public static void setValue(Annotation annotation, String annotationField, Object value) { + final Map memberValues = (Map) ReflectUtil.getFieldValue(Proxy.getInvocationHandler(annotation), "memberValues"); + memberValues.put(annotationField, value); + } + + /** + * 该注解对象是否为通过代理类生成的合成注解 + * + * @param annotation 注解对象 + * @return 是否 + * @see SynthesizedAnnotationProxy#isProxyAnnotation(Class) + */ + public static boolean isSynthesizedAnnotation(Annotation annotation) { + return SynthesizedAnnotationProxy.isProxyAnnotation(annotation.getClass()); + } + + /** + * 获取别名支持后的注解 + * + * @param annotationEle 被注解的类 + * @param annotationType 注解类型Class + * @param 注解类型 + * @return 别名支持后的注解 + * @since 5.7.23 + */ + public static T getAnnotationAlias(AnnotatedElement annotationEle, Class annotationType) { + final T annotation = getAnnotation(annotationEle, annotationType); + return aggregatingFromAnnotation(annotation).synthesize(annotationType); + } + + /** + * 将指定注解实例与其元注解转为合成注解 + * + * @param annotationType 注解类 + * @param annotations 注解对象 + * @param 注解类型 + * @return 合成注解 + * @see SynthesizedAggregateAnnotation + */ + public static T getSynthesizedAnnotation(Class annotationType, Annotation... annotations) { + // TODO 缓存合成注解信息,避免重复解析 + return Opt.ofNullable(annotations) + .filter(ArrayUtil::isNotEmpty) + .map(AnnotationUtil::aggregatingFromAnnotationWithMeta) + .map(a -> a.synthesize(annotationType)) + .get(); + } + + /** + *

获取元素上距离指定元素最接近的合成注解 + *

    + *
  • 若元素是类,则递归解析全部父类和全部父接口上的注解;
  • + *
  • 若元素是方法、属性或注解,则只解析其直接声明的注解;
  • + *
+ * + *

注解合成规则如下: + * 若{@code AnnotatedEle}按顺序从上到下声明了A,B,C三个注解,且三注解存在元注解如下: + *

+	 *    A -> M3
+	 *    B -> M1 -> M2 -> M3
+	 *    C -> M2 -> M3
+	 * 
+ * 此时入参{@code annotationType}类型为{@code M2},则最终将优先返回基于根注解B合成的合成注解 + * + * @param annotatedEle {@link AnnotatedElement},可以是Class、Method、Field、Constructor、ReflectPermission + * @param annotationType 注解类 + * @param 注解类型 + * @return 合成注解 + * @see SynthesizedAggregateAnnotation + */ + public static T getSynthesizedAnnotation(AnnotatedElement annotatedEle, Class annotationType) { + T target = annotatedEle.getAnnotation(annotationType); + if (ObjectUtil.isNotNull(target)) { + return target; + } + return AnnotationScanner.DIRECTLY + .getAnnotationsIfSupport(annotatedEle).stream() + .map(annotation -> getSynthesizedAnnotation(annotationType, annotation)) + .filter(Objects::nonNull) + .findFirst() + .orElse(null); + } + + /** + * 获取元素上所有指定注解 + *
    + *
  • 若元素是类,则递归解析全部父类和全部父接口上的注解;
  • + *
  • 若元素是方法、属性或注解,则只解析其直接声明的注解;
  • + *
+ * + *

注解合成规则如下: + * 若{@code AnnotatedEle}按顺序从上到下声明了A,B,C三个注解,且三注解存在元注解如下: + *

+	 *    A -> M1 -> M2
+	 *    B -> M3 -> M1 -> M2
+	 *    C -> M2
+	 * 
+ * 此时入参{@code annotationType}类型为{@code M1},则最终将返回基于根注解A与根注解B合成的合成注解。 + * + * @param annotatedEle {@link AnnotatedElement},可以是Class、Method、Field、Constructor、ReflectPermission + * @param annotationType 注解类 + * @param 注解类型 + * @return 合成注解 + * @see SynthesizedAggregateAnnotation + */ + public static List getAllSynthesizedAnnotations(AnnotatedElement annotatedEle, Class annotationType) { + return AnnotationScanner.DIRECTLY + .getAnnotationsIfSupport(annotatedEle).stream() + .map(annotation -> getSynthesizedAnnotation(annotationType, annotation)) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + } + + /** + * 对指定注解对象进行聚合 + * + * @param annotations 注解对象 + * @return 聚合注解 + */ + public static SynthesizedAggregateAnnotation aggregatingFromAnnotation(Annotation... annotations) { + return new GenericSynthesizedAggregateAnnotation(Arrays.asList(annotations), AnnotationScanner.NOTHING); + } + + /** + * 对指定注解对象及其元注解进行聚合 + * + * @param annotations 注解对象 + * @return 聚合注解 + */ + public static SynthesizedAggregateAnnotation aggregatingFromAnnotationWithMeta(Annotation... annotations) { + return new GenericSynthesizedAggregateAnnotation(Arrays.asList(annotations), AnnotationScanner.DIRECTLY_AND_META_ANNOTATION); + } + + /** + * 方法是否为注解属性方法。
+ * 方法无参数,且有返回值的方法认为是注解属性的方法。 + * + * @param method 方法 + */ + static boolean isAttributeMethod(Method method) { + return method.getParameterCount() == 0 && method.getReturnType() != void.class; + } + +} diff --git a/src/main/java/cn/hutool/core/annotation/CacheableAnnotationAttribute.java b/src/main/java/cn/hutool/core/annotation/CacheableAnnotationAttribute.java new file mode 100644 index 0000000..e4c8bc9 --- /dev/null +++ b/src/main/java/cn/hutool/core/annotation/CacheableAnnotationAttribute.java @@ -0,0 +1,63 @@ +package cn.hutool.core.annotation; + +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.ReflectUtil; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; + +/** + * {@link AnnotationAttribute}的基本实现 + * + * @author huangchengxing + */ +public class CacheableAnnotationAttribute implements AnnotationAttribute { + + private boolean valueInvoked; + private Object value; + + private boolean defaultValueInvoked; + private Object defaultValue; + + private final Annotation annotation; + private final Method attribute; + + public CacheableAnnotationAttribute(Annotation annotation, Method attribute) { + Assert.notNull(annotation, "annotation must not null"); + Assert.notNull(attribute, "attribute must not null"); + this.annotation = annotation; + this.attribute = attribute; + this.valueInvoked = false; + this.defaultValueInvoked = false; + } + + @Override + public Annotation getAnnotation() { + return this.annotation; + } + + @Override + public Method getAttribute() { + return this.attribute; + } + + @Override + public Object getValue() { + if (!valueInvoked) { + valueInvoked = true; + value = ReflectUtil.invoke(annotation, attribute); + } + return value; + } + + @Override + public boolean isValueEquivalentToDefaultValue() { + if (!defaultValueInvoked) { + defaultValue = attribute.getDefaultValue(); + defaultValueInvoked = true; + } + return ObjectUtil.equals(getValue(), defaultValue); + } + +} diff --git a/src/main/java/cn/hutool/core/annotation/CacheableSynthesizedAnnotationAttributeProcessor.java b/src/main/java/cn/hutool/core/annotation/CacheableSynthesizedAnnotationAttributeProcessor.java new file mode 100644 index 0000000..ab7ae9a --- /dev/null +++ b/src/main/java/cn/hutool/core/annotation/CacheableSynthesizedAnnotationAttributeProcessor.java @@ -0,0 +1,62 @@ +package cn.hutool.core.annotation; + +import cn.hutool.core.lang.Assert; +import cn.hutool.core.map.multi.RowKeyTable; +import cn.hutool.core.map.multi.Table; +import cn.hutool.core.util.ObjectUtil; + +import java.util.Collection; +import java.util.Comparator; + +/** + *

带缓存功能的{@link SynthesizedAnnotationAttributeProcessor}实现, + * 构建时需要传入比较器,获取属性值时将根据比较器对合成注解进行排序, + * 然后选择具有所需属性的,排序最靠前的注解用于获取属性值 + * + *

通过该处理器获取合成注解属性值时会出现隐式别名, + * 即子注解和元注解中同时存在类型和名称皆相同的属性时,元注解中属性总是会被该属性覆盖, + * 并且该覆盖关系并不会通过{@link Alias}或{@link Link}被传递到关联的属性中。 + * + * @author huangchengxing + */ +public class CacheableSynthesizedAnnotationAttributeProcessor implements SynthesizedAnnotationAttributeProcessor { + + private final Table, Object> valueCaches = new RowKeyTable<>(); + private final Comparator annotationComparator; + + /** + * 创建一个带缓存的注解值选择器 + * + * @param annotationComparator 注解比较器,排序更靠前的注解将被优先用于获取值 + */ + public CacheableSynthesizedAnnotationAttributeProcessor(Comparator annotationComparator) { + Assert.notNull(annotationComparator, "annotationComparator must not null"); + this.annotationComparator = annotationComparator; + } + + /** + * 创建一个带缓存的注解值选择器, + * 默认按{@link SynthesizedAnnotation#getVerticalDistance()}和{@link SynthesizedAnnotation#getHorizontalDistance()}排序, + * 越靠前的越优先被取值。 + */ + public CacheableSynthesizedAnnotationAttributeProcessor() { + this(Hierarchical.DEFAULT_HIERARCHICAL_COMPARATOR); + } + + @SuppressWarnings("unchecked") + @Override + public T getAttributeValue(String attributeName, Class attributeType, Collection synthesizedAnnotations) { + Object value = valueCaches.get(attributeName, attributeType); + // 此处理论上不可能出现缓存值为nul的情况 + if (ObjectUtil.isNotNull(value)) { + return (T)value; + } + value = synthesizedAnnotations.stream() + .filter(ma -> ma.hasAttribute(attributeName, attributeType)) + .min(annotationComparator) + .map(ma -> ma.getAttributeValue(attributeName)) + .orElse(null); + valueCaches.put(attributeName, attributeType, value); + return (T)value; + } +} diff --git a/src/main/java/cn/hutool/core/annotation/CombinationAnnotationElement.java b/src/main/java/cn/hutool/core/annotation/CombinationAnnotationElement.java new file mode 100644 index 0000000..e5c1f47 --- /dev/null +++ b/src/main/java/cn/hutool/core/annotation/CombinationAnnotationElement.java @@ -0,0 +1,165 @@ +package cn.hutool.core.annotation; + +import cn.hutool.core.map.TableMap; + +import java.io.Serializable; +import java.lang.annotation.Annotation; +import java.lang.reflect.AnnotatedElement; +import java.util.Arrays; +import java.util.Collection; +import java.util.Map; +import java.util.function.Predicate; + +/** + * 组合注解 对JDK的原生注解机制做一个增强,支持类似Spring的组合注解。
+ * 核心实现使用了递归获取指定元素上的注解以及注解的注解,以实现复合注解的获取。 + * + * @author Succy, Looly + * @since 4.0.9 + **/ + +public class CombinationAnnotationElement implements AnnotatedElement, Serializable { + private static final long serialVersionUID = 1L; + + /** + * 创建CombinationAnnotationElement + * + * @param element 需要解析注解的元素:可以是Class、Method、Field、Constructor、ReflectPermission + * @param predicate 过滤器,{@link Predicate#test(Object)}返回{@code true}保留,否则不保留 + * @return CombinationAnnotationElement + * @since 5.8.0 + */ + public static CombinationAnnotationElement of(AnnotatedElement element, Predicate predicate) { + return new CombinationAnnotationElement(element, predicate); + } + + /** + * 注解类型与注解对象对应表 + */ + private Map, Annotation> annotationMap; + /** + * 直接注解类型与注解对象对应表 + */ + private Map, Annotation> declaredAnnotationMap; + /** + * 过滤器 + */ + private final Predicate predicate; + + /** + * 构造 + * + * @param element 需要解析注解的元素:可以是Class、Method、Field、Constructor、ReflectPermission + */ + public CombinationAnnotationElement(AnnotatedElement element) { + this(element, null); + } + + /** + * 构造 + * + * @param element 需要解析注解的元素:可以是Class、Method、Field、Constructor、ReflectPermission + * @param predicate 过滤器,{@link Predicate#test(Object)}返回{@code true}保留,否则不保留 + * @since 5.8.0 + */ + public CombinationAnnotationElement(AnnotatedElement element, Predicate predicate) { + this.predicate = predicate; + init(element); + } + + @Override + public boolean isAnnotationPresent(Class annotationClass) { + return annotationMap.containsKey(annotationClass); + } + + @Override + @SuppressWarnings("unchecked") + public T getAnnotation(Class annotationClass) { + Annotation annotation = annotationMap.get(annotationClass); + return (annotation == null) ? null : (T) annotation; + } + + @Override + public Annotation[] getAnnotations() { + final Collection annotations = this.annotationMap.values(); + return annotations.toArray(new Annotation[0]); + } + + @Override + public Annotation[] getDeclaredAnnotations() { + final Collection annotations = this.declaredAnnotationMap.values(); + return annotations.toArray(new Annotation[0]); + } + + /** + * 初始化 + * + * @param element 元素 + */ + private void init(AnnotatedElement element) { + final Annotation[] declaredAnnotations = element.getDeclaredAnnotations(); + this.declaredAnnotationMap = new TableMap<>(); + parseDeclared(declaredAnnotations); + + final Annotation[] annotations = element.getAnnotations(); + if (Arrays.equals(declaredAnnotations, annotations)) { + this.annotationMap = this.declaredAnnotationMap; + } else { + this.annotationMap = new TableMap<>(); + parse(annotations); + } + } + + /** + * 进行递归解析注解,直到全部都是元注解为止 + * + * @param annotations Class, Method, Field等 + */ + private void parseDeclared(Annotation[] annotations) { + Class annotationType; + // 直接注解 + for (Annotation annotation : annotations) { + annotationType = annotation.annotationType(); + // issue#I5FQGW@Gitee:跳过元注解和已经处理过的注解,防止递归调用 + if (AnnotationUtil.isNotJdkMateAnnotation(annotationType) + && !declaredAnnotationMap.containsKey(annotationType)) { + if(test(annotation)){ + declaredAnnotationMap.put(annotationType, annotation); + } + // 测试不通过的注解,不影响继续递归 + parseDeclared(annotationType.getDeclaredAnnotations()); + } + } + } + + /** + * 进行递归解析注解,直到全部都是元注解为止 + * + * @param annotations Class, Method, Field等 + */ + private void parse(Annotation[] annotations) { + Class annotationType; + for (Annotation annotation : annotations) { + annotationType = annotation.annotationType(); + // issue#I5FQGW@Gitee:跳过元注解和已经处理过的注解,防止递归调用 + if (AnnotationUtil.isNotJdkMateAnnotation(annotationType) + && !declaredAnnotationMap.containsKey(annotationType)) { + if(test(annotation)){ + annotationMap.put(annotationType, annotation); + } + // 测试不通过的注解,不影响继续递归 + parse(annotationType.getAnnotations()); + } + } + } + + /** + * 检查给定的注解是否符合过滤条件 + * + * @param annotation 注解对象 + * @return 是否符合条件 + */ + private boolean test(Annotation annotation) { + return null == this.predicate || this.predicate.test(annotation); + } +} diff --git a/src/main/java/cn/hutool/core/annotation/ForceAliasFor.java b/src/main/java/cn/hutool/core/annotation/ForceAliasFor.java new file mode 100644 index 0000000..7549a15 --- /dev/null +++ b/src/main/java/cn/hutool/core/annotation/ForceAliasFor.java @@ -0,0 +1,35 @@ +package cn.hutool.core.annotation; + +import java.lang.annotation.*; + +/** + *

{@link Link}的子注解。表示“原始属性”将强制作为“关联属性”的别名。效果等同于在“原始属性”上添加{@link Alias}注解, + * 任何情况下,获取“关联属性”的值都将直接返回“原始属性”的值 + * 注意,该注解与{@link Link}、{@link AliasFor}或{@link MirrorFor}一起使用时,将只有被声明在最上面的注解会生效 + * + * @author huangchengxing + * @see Link + * @see RelationType#FORCE_ALIAS_FOR + */ +@Link(type = RelationType.FORCE_ALIAS_FOR) +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE}) +public @interface ForceAliasFor { + + /** + * 产生关联的注解类型,当不指定时,默认指注释的属性所在的类 + * + * @return 关联注解类型 + */ + @Link(annotation = Link.class, attribute = "annotation", type = RelationType.FORCE_ALIAS_FOR) + Class annotation() default Annotation.class; + + /** + * {@link #annotation()}指定注解中关联的属性 + * + * @return 关联的属性 + */ + @Link(annotation = Link.class, attribute = "attribute", type = RelationType.FORCE_ALIAS_FOR) + String attribute() default ""; +} diff --git a/src/main/java/cn/hutool/core/annotation/ForceAliasedAnnotationAttribute.java b/src/main/java/cn/hutool/core/annotation/ForceAliasedAnnotationAttribute.java new file mode 100644 index 0000000..312219c --- /dev/null +++ b/src/main/java/cn/hutool/core/annotation/ForceAliasedAnnotationAttribute.java @@ -0,0 +1,49 @@ +package cn.hutool.core.annotation; + +/** + * 表示一个被指定了强制别名的注解属性。 + * 当调用{@link #getValue()}时,总是返回{@link #linked}的值 + * + * @author huangchengxing + * @see AliasAnnotationPostProcessor + * @see AliasLinkAnnotationPostProcessor + * @see RelationType#ALIAS_FOR + * @see RelationType#FORCE_ALIAS_FOR + */ +public class ForceAliasedAnnotationAttribute extends AbstractWrappedAnnotationAttribute { + + protected ForceAliasedAnnotationAttribute(AnnotationAttribute origin, AnnotationAttribute linked) { + super(origin, linked); + } + + /** + * 总是返回{@link #linked}的{@link AnnotationAttribute#getValue()}的返回值 + * + * @return {@link #linked}的{@link AnnotationAttribute#getValue()}的返回值 + */ + @Override + public Object getValue() { + return linked.getValue(); + } + + /** + * 总是返回{@link #linked}的{@link AnnotationAttribute#isValueEquivalentToDefaultValue()}的返回值 + * + * @return {@link #linked}的{@link AnnotationAttribute#isValueEquivalentToDefaultValue()}的返回值 + */ + @Override + public boolean isValueEquivalentToDefaultValue() { + return linked.isValueEquivalentToDefaultValue(); + } + + /** + * 总是返回{@link #linked}的{@link AnnotationAttribute#getAttributeType()}的返回值 + * + * @return {@link #linked}的{@link AnnotationAttribute#getAttributeType()}的返回值 + */ + @Override + public Class getAttributeType() { + return linked.getAttributeType(); + } + +} diff --git a/src/main/java/cn/hutool/core/annotation/GenericSynthesizedAggregateAnnotation.java b/src/main/java/cn/hutool/core/annotation/GenericSynthesizedAggregateAnnotation.java new file mode 100644 index 0000000..8a5f7b7 --- /dev/null +++ b/src/main/java/cn/hutool/core/annotation/GenericSynthesizedAggregateAnnotation.java @@ -0,0 +1,318 @@ +package cn.hutool.core.annotation; + +import cn.hutool.core.annotation.scanner.AnnotationScanner; +import cn.hutool.core.annotation.scanner.MetaAnnotationScanner; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.lang.Opt; +import cn.hutool.core.util.ObjectUtil; + +import java.lang.annotation.Annotation; +import java.lang.reflect.AnnotatedElement; +import java.util.*; + +/** + * {@link SynthesizedAggregateAnnotation}的基本实现,表示基于多个注解对象, + * 或多个根注解对象与他们的多层元注解对象的聚合得到的注解。 + * + *

假设现有注解A,若指定的{@link #annotationScanner}支持扫描注解A的元注解, + * 且A上存在元注解B,B上存在元注解C,则对注解A进行解析,将得到包含根注解A,以及其元注解B、C在内的合成元注解聚合{@link GenericSynthesizedAggregateAnnotation}。 + * 从{@link AnnotatedElement}的角度来说,得到的合成注解是一个同时承载有ABC三个注解对象的被注解元素, + * 因此通过调用{@link AnnotatedElement}的相关方法将返回对应符合语义的注解对象。 + * + *

在扫描指定根注解及其元注解时,若在不同的层级出现了类型相同的注解实例, + * 将会根据实例化时指定的{@link SynthesizedAnnotationSelector}选择最优的注解, + * 完成对根注解及其元注解的扫描后,合成注解中每种类型的注解对象都将有且仅有一个。
+ * 默认情况下,将使用{@link SynthesizedAnnotationSelector#NEAREST_AND_OLDEST_PRIORITY}作为选择器, + * 此时若出现扫描时得到了多个同类型的注解对象,有且仅有最接近根注解的注解对象会被作为有效注解。 + * + *

当扫描的注解对象经过{@link SynthesizedAnnotationSelector}处理后, + * 将会被转为{@link MetaAnnotation},并使用在实例化时指定的{@link AliasAnnotationPostProcessor} + * 进行后置处理。
+ * 默认情况下,将注册以下后置处理器以对{@link Alias}与{@link Link}和其扩展注解提供支持: + *

    + *
  • {@link AliasAnnotationPostProcessor};
  • + *
  • {@link MirrorLinkAnnotationPostProcessor};
  • + *
  • {@link AliasLinkAnnotationPostProcessor};
  • + *
+ * 若用户需要自行扩展,则需要保证上述三个处理器被正确注入当前实例。 + * + *

{@link GenericSynthesizedAggregateAnnotation}支持通过{@link #getAttributeValue(String, Class)}, + * 或通过{@link #synthesize(Class)}获得注解代理对象后获取指定类型的注解属性值, + * 返回的属性值将根据合成注解中对应原始注解属性上的{@link Alias}与{@link Link}注解而有所变化。 + * 通过当前实例获取属性值时,将经过{@link SynthesizedAnnotationAttributeProcessor}的处理。
+ * 默认情况下,实例将会注册{@link CacheableSynthesizedAnnotationAttributeProcessor}, + * 该处理器将令元注解中与子注解类型与名称皆一致的属性被子注解的属性覆盖,并且缓存最终获取到的属性值。 + * + * @author huangchengxing + * @see AnnotationUtil + * @see SynthesizedAnnotationProxy + * @see SynthesizedAnnotationSelector + * @see SynthesizedAnnotationAttributeProcessor + * @see SynthesizedAnnotationPostProcessor + * @see AnnotationSynthesizer + * @see AnnotationScanner + */ +public class GenericSynthesizedAggregateAnnotation + extends AbstractAnnotationSynthesizer> + implements SynthesizedAggregateAnnotation { + + /** + * 根对象 + */ + private final Object root; + + /** + * 距离根对象的垂直距离 + */ + private final int verticalDistance; + + /** + * 距离根对象的水平距离 + */ + private final int horizontalDistance; + + /** + * 合成注解属性处理器 + */ + private final SynthesizedAnnotationAttributeProcessor attributeProcessor; + + /** + * 基于指定根注解,为其与其元注解的层级结构中的全部注解构造一个合成注解。 + * 当层级结构中出现了相同的注解对象时,将优先选择以距离根注解最近,且优先被扫描的注解对象, + * 当获取值时,同样遵循该规则。 + * + * @param source 源注解 + */ + public GenericSynthesizedAggregateAnnotation(Annotation... source) { + this(Arrays.asList(source), new MetaAnnotationScanner()); + } + + /** + * 基于指定根注解,为其层级结构中的全部注解构造一个合成注解。 + * 若扫描器支持对注解的层级结构进行扫描,则若层级结构中出现了相同的注解对象时, + * 将优先选择以距离根注解最近,且优先被扫描的注解对象,并且当获取注解属性值时同样遵循该规则。 + * + * @param source 源注解 + * @param annotationScanner 注解扫描器,该扫描器必须支持扫描注解类 + */ + public GenericSynthesizedAggregateAnnotation(List source, AnnotationScanner annotationScanner) { + this( + source, SynthesizedAnnotationSelector.NEAREST_AND_OLDEST_PRIORITY, + new CacheableSynthesizedAnnotationAttributeProcessor(), + Arrays.asList( + SynthesizedAnnotationPostProcessor.ALIAS_ANNOTATION_POST_PROCESSOR, + SynthesizedAnnotationPostProcessor.MIRROR_LINK_ANNOTATION_POST_PROCESSOR, + SynthesizedAnnotationPostProcessor.ALIAS_LINK_ANNOTATION_POST_PROCESSOR + ), + annotationScanner + ); + } + + /** + * 基于指定根注解,为其层级结构中的全部注解构造一个合成注解 + * + * @param source 当前查找的注解对象 + * @param annotationSelector 合成注解选择器 + * @param attributeProcessor 注解属性处理器 + * @param annotationPostProcessors 注解后置处理器 + * @param annotationScanner 注解扫描器,该扫描器必须支持扫描注解类 + */ + public GenericSynthesizedAggregateAnnotation( + List source, + SynthesizedAnnotationSelector annotationSelector, + SynthesizedAnnotationAttributeProcessor attributeProcessor, + Collection annotationPostProcessors, + AnnotationScanner annotationScanner) { + this( + null, 0, 0, + source, annotationSelector, attributeProcessor, annotationPostProcessors, annotationScanner + ); + } + + /** + * 基于指定根注解,为其层级结构中的全部注解构造一个合成注解 + * + * @param root 根对象 + * @param verticalDistance 距离根对象的水平距离 + * @param horizontalDistance 距离根对象的垂直距离 + * @param source 当前查找的注解对象 + * @param annotationSelector 合成注解选择器 + * @param attributeProcessor 注解属性处理器 + * @param annotationPostProcessors 注解后置处理器 + * @param annotationScanner 注解扫描器,该扫描器必须支持扫描注解类 + */ + GenericSynthesizedAggregateAnnotation( + Object root, int verticalDistance, int horizontalDistance, + List source, + SynthesizedAnnotationSelector annotationSelector, + SynthesizedAnnotationAttributeProcessor attributeProcessor, + Collection annotationPostProcessors, + AnnotationScanner annotationScanner) { + super(source, annotationSelector, annotationPostProcessors, annotationScanner); + Assert.notNull(attributeProcessor, "attributeProcessor must not null"); + + this.root = ObjectUtil.defaultIfNull(root, this); + this.verticalDistance = verticalDistance; + this.horizontalDistance = horizontalDistance; + this.attributeProcessor = attributeProcessor; + } + + /** + * 获取根对象 + * + * @return 根对象 + */ + @Override + public Object getRoot() { + return root; + } + + /** + * 获取与根对象的垂直距离 + * + * @return 与根对象的垂直距离 + */ + @Override + public int getVerticalDistance() { + return verticalDistance; + } + + /** + * 获取与根对象的水平距离 + * + * @return 获取与根对象的水平距离 + */ + @Override + public int getHorizontalDistance() { + return horizontalDistance; + } + + /** + * 按广度优先扫描{@link #source}上的元注解 + */ + @Override + protected Map, SynthesizedAnnotation> loadAnnotations() { + Map, SynthesizedAnnotation> annotationMap = new LinkedHashMap<>(); + + // 根注解默认水平坐标为0,根注解的元注解坐标从1开始 + for (int i = 0; i < source.size(); i++) { + final Annotation sourceAnnotation = source.get(i); + Assert.isFalse(AnnotationUtil.isSynthesizedAnnotation(sourceAnnotation), "source [{}] has been synthesized"); + annotationMap.put(sourceAnnotation.annotationType(), new MetaAnnotation(sourceAnnotation, sourceAnnotation, 0, i)); + Assert.isTrue( + annotationScanner.support(sourceAnnotation.annotationType()), + "annotation scanner [{}] cannot support scan [{}]", + annotationScanner, sourceAnnotation.annotationType() + ); + annotationScanner.scan( + (index, annotation) -> { + SynthesizedAnnotation oldAnnotation = annotationMap.get(annotation.annotationType()); + SynthesizedAnnotation newAnnotation = new MetaAnnotation(sourceAnnotation, annotation, index + 1, annotationMap.size()); + if (ObjectUtil.isNull(oldAnnotation)) { + annotationMap.put(annotation.annotationType(), newAnnotation); + } else { + annotationMap.put(annotation.annotationType(), annotationSelector.choose(oldAnnotation, newAnnotation)); + } + }, + sourceAnnotation.annotationType(), null + ); + } + return annotationMap; + } + + /** + * 获取合成注解属性处理器 + * + * @return 合成注解属性处理器 + */ + @Override + public SynthesizedAnnotationAttributeProcessor getAnnotationAttributeProcessor() { + return this.attributeProcessor; + } + + /** + * 根据指定的属性名与属性类型获取对应的属性值,若存在{@link Alias}则获取{@link Alias#value()}指定的别名属性的值 + *

当不同层级的注解之间存在同名同类型属性时,将优先获取更接近根注解的属性 + * + * @param attributeName 属性名 + * @param attributeType 属性类型 + * @return 属性 + */ + @Override + public Object getAttributeValue(String attributeName, Class attributeType) { + return attributeProcessor.getAttributeValue(attributeName, attributeType, synthesizedAnnotationMap.values()); + } + + /** + * 获取合成注解中包含的指定注解 + * + * @param annotationType 注解类型 + * @param 注解类型 + * @return 注解对象 + */ + @Override + public T getAnnotation(Class annotationType) { + return Opt.ofNullable(annotationType) + .map(synthesizedAnnotationMap::get) + .map(SynthesizedAnnotation::getAnnotation) + .map(annotationType::cast) + .orElse(null); + } + + /** + * 当前合成注解中是否存在指定元注解 + * + * @param annotationType 注解类型 + * @return 是否 + */ + @Override + public boolean isAnnotationPresent(Class annotationType) { + return synthesizedAnnotationMap.containsKey(annotationType); + } + + /** + * 获取合成注解中包含的全部注解 + * + * @return 注解对象 + */ + @Override + public Annotation[] getAnnotations() { + return synthesizedAnnotationMap.values().stream() + .map(SynthesizedAnnotation::getAnnotation) + .toArray(Annotation[]::new); + } + + /** + * 若合成注解在存在指定元注解,则使用动态代理生成一个对应的注解实例 + * + * @param annotationType 注解类型 + * @return 合成注解对象 + * @see SynthesizedAnnotationProxy#create(Class, AnnotationAttributeValueProvider, SynthesizedAnnotation) + */ + @Override + public T synthesize(Class annotationType, SynthesizedAnnotation annotation) { + return SynthesizedAnnotationProxy.create(annotationType, this, annotation); + } + + /** + * 注解包装类,表示{@link #source}以及{@link #source}所属层级结构中的全部关联注解对象 + * + * @author huangchengxing + */ + public static class MetaAnnotation extends GenericSynthesizedAnnotation { + + /** + * 创建一个合成注解 + * + * @param root 根对象 + * @param annotation 被合成的注解对象 + * @param verticalDistance 距离根对象的水平距离 + * @param horizontalDistance 距离根对象的垂直距离 + */ + protected MetaAnnotation(Annotation root, Annotation annotation, int verticalDistance, int horizontalDistance) { + super(root, annotation, verticalDistance, horizontalDistance); + } + + } + +} diff --git a/src/main/java/cn/hutool/core/annotation/GenericSynthesizedAnnotation.java b/src/main/java/cn/hutool/core/annotation/GenericSynthesizedAnnotation.java new file mode 100644 index 0000000..3034249 --- /dev/null +++ b/src/main/java/cn/hutool/core/annotation/GenericSynthesizedAnnotation.java @@ -0,0 +1,197 @@ +package cn.hutool.core.annotation; + +import cn.hutool.core.lang.Opt; +import cn.hutool.core.util.ClassUtil; +import cn.hutool.core.util.ObjectUtil; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; +import java.util.HashMap; +import java.util.Map; +import java.util.function.UnaryOperator; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * {@link SynthesizedAnnotation}的基本实现 + * + * @param 根对象类型 + * @param 注解类型 + * @author huangchengxing + */ +public class GenericSynthesizedAnnotation implements SynthesizedAnnotation { + + private final R root; + private final T annotation; + private final Map attributeMethodCaches; + private final int verticalDistance; + private final int horizontalDistance; + + /** + * 创建一个合成注解 + * + * @param root 根对象 + * @param annotation 被合成的注解对象 + * @param verticalDistance 距离根对象的水平距离 + * @param horizontalDistance 距离根对象的垂直距离 + */ + protected GenericSynthesizedAnnotation( + R root, T annotation, int verticalDistance, int horizontalDistance) { + this.root = root; + this.annotation = annotation; + this.verticalDistance = verticalDistance; + this.horizontalDistance = horizontalDistance; + this.attributeMethodCaches = new HashMap<>(); + this.attributeMethodCaches.putAll(loadAttributeMethods()); + } + + /** + * 加载注解属性 + * + * @return 注解属性 + */ + protected Map loadAttributeMethods() { + return Stream.of(ClassUtil.getDeclaredMethods(annotation.annotationType())) + .filter(AnnotationUtil::isAttributeMethod) + .collect(Collectors.toMap(Method::getName, method -> new CacheableAnnotationAttribute(annotation, method))); + } + + /** + * 元注解是否存在该属性 + * + * @param attributeName 属性名 + * @return 是否存在该属性 + */ + public boolean hasAttribute(String attributeName) { + return attributeMethodCaches.containsKey(attributeName); + } + + /** + * 元注解是否存在该属性,且该属性的值类型是指定类型或其子类 + * + * @param attributeName 属性名 + * @param returnType 返回值类型 + * @return 是否存在该属性 + */ + @Override + public boolean hasAttribute(String attributeName, Class returnType) { + return Opt.ofNullable(attributeMethodCaches.get(attributeName)) + .filter(method -> ClassUtil.isAssignable(returnType, method.getAttributeType())) + .isPresent(); + } + + /** + * 获取该注解的全部属性 + * + * @return 注解属性 + */ + @Override + public Map getAttributes() { + return this.attributeMethodCaches; + } + + /** + * 设置属性值 + * + * @param attributeName 属性名称 + * @param attribute 注解属性 + */ + @Override + public void setAttribute(String attributeName, AnnotationAttribute attribute) { + attributeMethodCaches.put(attributeName, attribute); + } + + /** + * 替换属性值 + * + * @param attributeName 属性名 + * @param operator 替换操作 + */ + @Override + public void replaceAttribute(String attributeName, UnaryOperator operator) { + AnnotationAttribute old = attributeMethodCaches.get(attributeName); + if (ObjectUtil.isNotNull(old)) { + attributeMethodCaches.put(attributeName, operator.apply(old)); + } + } + + /** + * 获取属性值 + * + * @param attributeName 属性名 + * @return 属性值 + */ + @Override + public Object getAttributeValue(String attributeName) { + return Opt.ofNullable(attributeMethodCaches.get(attributeName)) + .map(AnnotationAttribute::getValue) + .get(); + } + + /** + * 获取该合成注解对应的根节点 + * + * @return 合成注解对应的根节点 + */ + @Override + public R getRoot() { + return root; + } + + /** + * 获取被合成的注解对象 + * + * @return 注解对象 + */ + @Override + public T getAnnotation() { + return annotation; + } + + /** + * 获取该合成注解与根对象的垂直距离。 + * 默认情况下,该距离即为当前注解与根对象之间相隔的层级数。 + * + * @return 合成注解与根对象的垂直距离 + */ + @Override + public int getVerticalDistance() { + return verticalDistance; + } + + /** + * 获取该合成注解与根对象的水平距离。 + * 默认情况下,该距离即为当前注解与根对象之间相隔的已经被扫描到的注解数。 + * + * @return 合成注解与根对象的水平距离 + */ + @Override + public int getHorizontalDistance() { + return horizontalDistance; + } + + /** + * 获取被合成的注解类型 + * + * @return 被合成的注解类型 + */ + @Override + public Class annotationType() { + return annotation.annotationType(); + } + + /** + * 获取注解属性值 + * + * @param attributeName 属性名称 + * @param attributeType 属性类型 + * @return 注解属性值 + */ + @Override + public Object getAttributeValue(String attributeName, Class attributeType) { + return Opt.ofNullable(attributeMethodCaches.get(attributeName)) + .filter(method -> ClassUtil.isAssignable(attributeType, method.getAttributeType())) + .map(AnnotationAttribute::getValue) + .get(); + } +} diff --git a/src/main/java/cn/hutool/core/annotation/Hierarchical.java b/src/main/java/cn/hutool/core/annotation/Hierarchical.java new file mode 100644 index 0000000..00c9938 --- /dev/null +++ b/src/main/java/cn/hutool/core/annotation/Hierarchical.java @@ -0,0 +1,155 @@ +package cn.hutool.core.annotation; + + +import java.util.Comparator; + +/** + *

描述以一个参照物为对象,存在于该参照物的层级结构中的对象。 + * + *

该对象可通过{@link #getVerticalDistance()}与{@link #getHorizontalDistance()} + * 描述其在以参照物为基点的坐标坐标系中的位置。
+ * 在需要对该接口的实现类进行按优先级排序时,距离{@link #getRoot()}对象越近,则该实现类的优先级越高。 + * 默认提供了{@link #DEFAULT_HIERARCHICAL_COMPARATOR}用于实现该比较规则。
+ * 一般情况下,{@link #getRoot()}返回值相同的对象之间的比较才有意义。 + * + *

此外,还提供了{@link Selector}接口用于根据一定的规则从两个{@link Hierarchical}实现类中选择并返回一个最合适的对象, + * 默认提供了四个实现类: + *

    + *
  • {@link Selector#NEAREST_AND_OLDEST_PRIORITY}: 返回距离根对象更近的对象,当距离一样时优先返回旧对象;
  • + *
  • {@link Selector#NEAREST_AND_NEWEST_PRIORITY}: 返回距离根对象更近的对象,当距离一样时优先返回新对象;
  • + *
  • {@link Selector#FARTHEST_AND_OLDEST_PRIORITY}: 返回距离根对象更远的对象,当距离一样时优先返回旧对象;
  • + *
  • {@link Selector#FARTHEST_AND_NEWEST_PRIORITY}: 返回距离根对象更远的对象,当距离一样时优先返回新对象;
  • + *
+ * + * @author huangchengxing + */ +public interface Hierarchical extends Comparable { + + // ====================== compare ====================== + + /** + * 默认{@link #getHorizontalDistance()}与{@link #getVerticalDistance()}排序的比较器 + */ + Comparator DEFAULT_HIERARCHICAL_COMPARATOR = Comparator + .comparing(Hierarchical::getVerticalDistance) + .thenComparing(Hierarchical::getHorizontalDistance); + + /** + * 按{@link #getVerticalDistance()}和{@link #getHorizontalDistance()}排序 + * + * @param o {@link SynthesizedAnnotation}对象 + * @return 比较值 + */ + @Override + default int compareTo(Hierarchical o) { + return DEFAULT_HIERARCHICAL_COMPARATOR.compare(this, o); + } + + // ====================== hierarchical ====================== + + /** + * 参照物,即坐标为{@code (0, 0)}的对象。 + * 当对象本身即为参照物时,该方法应当返回其本身 + * + * @return 参照物 + */ + Object getRoot(); + + /** + * 获取该对象与参照物的垂直距离。 + * 默认情况下,该距离即为当前对象与参照物之间相隔的层级数。 + * + * @return 合成注解与根对象的垂直距离 + */ + int getVerticalDistance(); + + /** + * 获取该对象与参照物的水平距离。 + * 默认情况下,该距离即为当前对象在与参照物{@link #getVerticalDistance()}相同的情况下条, + * 该对象被扫描到的顺序。 + * + * @return 合成注解与根对象的水平距离 + */ + int getHorizontalDistance(); + + // ====================== selector ====================== + + /** + * {@link Hierarchical}选择器,用于根据一定的规则从两个{@link Hierarchical}实现类中选择并返回一个最合适的对象 + */ + @FunctionalInterface + interface Selector { + + /** + * 返回距离根对象更近的对象,当距离一样时优先返回旧对象 + */ + Selector NEAREST_AND_OLDEST_PRIORITY = new NearestAndOldestPrioritySelector(); + + /** + * 返回距离根对象更近的对象,当距离一样时优先返回新对象 + */ + Selector NEAREST_AND_NEWEST_PRIORITY = new NearestAndNewestPrioritySelector(); + + /** + * 返回距离根对象更远的对象,当距离一样时优先返回旧对象 + */ + Selector FARTHEST_AND_OLDEST_PRIORITY = new FarthestAndOldestPrioritySelector(); + + /** + * 返回距离根对象更远的对象,当距离一样时优先返回新对象 + */ + Selector FARTHEST_AND_NEWEST_PRIORITY = new FarthestAndNewestPrioritySelector(); + + /** + * 比较两个被合成的对象,选择其中的一个并返回 + * + * @param 复合注解类型 + * @param prev 上一对象,该参数不允许为空 + * @param next 下一对象,该参数不允许为空 + * @return 对象 + */ + T choose(T prev, T next); + + /** + * 返回距离根对象更近的注解,当距离一样时优先返回旧注解 + */ + class NearestAndOldestPrioritySelector implements Selector { + @Override + public T choose(T oldAnnotation, T newAnnotation) { + return newAnnotation.getVerticalDistance() < oldAnnotation.getVerticalDistance() ? newAnnotation : oldAnnotation; + } + } + + /** + * 返回距离根对象更近的注解,当距离一样时优先返回新注解 + */ + class NearestAndNewestPrioritySelector implements Selector { + @Override + public T choose(T oldAnnotation, T newAnnotation) { + return newAnnotation.getVerticalDistance() <= oldAnnotation.getVerticalDistance() ? newAnnotation : oldAnnotation; + } + } + + /** + * 返回距离根对象更远的注解,当距离一样时优先返回旧注解 + */ + class FarthestAndOldestPrioritySelector implements Selector { + @Override + public T choose(T oldAnnotation, T newAnnotation) { + return newAnnotation.getVerticalDistance() > oldAnnotation.getVerticalDistance() ? newAnnotation : oldAnnotation; + } + } + + /** + * 返回距离根对象更远的注解,当距离一样时优先返回新注解 + */ + class FarthestAndNewestPrioritySelector implements Selector { + @Override + public T choose(T oldAnnotation, T newAnnotation) { + return newAnnotation.getVerticalDistance() >= oldAnnotation.getVerticalDistance() ? newAnnotation : oldAnnotation; + } + } + + } + +} diff --git a/src/main/java/cn/hutool/core/annotation/Link.java b/src/main/java/cn/hutool/core/annotation/Link.java new file mode 100644 index 0000000..8f4e20f --- /dev/null +++ b/src/main/java/cn/hutool/core/annotation/Link.java @@ -0,0 +1,49 @@ +package cn.hutool.core.annotation; + +import java.lang.annotation.*; + +/** + *

用于在同一注解中,或具有一定关联的不同注解的属性中,表明这些属性之间具有特定的关联关系。 + * 在通过{@link SynthesizedAggregateAnnotation}获取合成注解后,合成注解获取属性值时会根据该注解进行调整。
+ * + *

该注解存在三个字注解:{@link MirrorFor}、{@link ForceAliasFor}或{@link AliasFor}, + * 使用三个子注解等同于{@link Link}。但是需要注意的是, + * 当注解中的属性同时存在多个{@link Link}或基于{@link Link}的子注解时, + * 仅有声明在被注解的属性最上方的注解会生效,其余注解都将被忽略。 + * + * 注意:该注解的优先级低于{@link Alias} + * + * @author huangchengxing + * @see SynthesizedAggregateAnnotation + * @see RelationType + * @see AliasFor + * @see MirrorFor + * @see ForceAliasFor + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE}) +public @interface Link { + + /** + * 产生关联的注解类型,当不指定时,默认指注释的属性所在的类 + * + * @return 关联的注解类型 + */ + Class annotation() default Annotation.class; + + /** + * {@link #annotation()}指定注解中关联的属性 + * + * @return 属性名 + */ + String attribute() default ""; + + /** + * {@link #attribute()}指定属性与当前注解的属性建的关联关系类型 + * + * @return 关系类型 + */ + RelationType type() default RelationType.MIRROR_FOR; + +} diff --git a/src/main/java/cn/hutool/core/annotation/MirrorFor.java b/src/main/java/cn/hutool/core/annotation/MirrorFor.java new file mode 100644 index 0000000..7d69b34 --- /dev/null +++ b/src/main/java/cn/hutool/core/annotation/MirrorFor.java @@ -0,0 +1,42 @@ +package cn.hutool.core.annotation; + +import java.lang.annotation.*; + +/** + *

{@link Link}的子注解。表示注解的属性与指定的属性互为镜像,通过一个属性将能够获得对方的值。
+ * 它们遵循下述规则: + *

    + *
  • 互为镜像的两个属性,必须同时通过指定模式为{@code MIRROR_FOR}的{@link Link}注解指定对方;
  • + *
  • 互为镜像的两个属性,类型必须一致;
  • + *
  • 互为镜像的两个属性在获取值,且两者的值皆不同时,必须且仅允许有一个非默认值,该值被优先返回;
  • + *
  • 互为镜像的两个属性,在值都为默认值或都不为默认值时,两者的值必须相等;
  • + *
+ * 注意,该注解与{@link Link}、{@link ForceAliasFor}或{@link AliasFor}一起使用时,将只有被声明在最上面的注解会生效 + * + * @author huangchengxing + * @see Link + * @see RelationType#MIRROR_FOR + */ +@Link(type = RelationType.MIRROR_FOR) +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE}) +public @interface MirrorFor { + + /** + * 产生关联的注解类型,当不指定时,默认指注释的属性所在的类 + * + * @return 关联的注解类型 + */ + @Link(annotation = Link.class, attribute = "annotation", type = RelationType.FORCE_ALIAS_FOR) + Class annotation() default Annotation.class; + + /** + * {@link #annotation()}指定注解中关联的属性 + * + * @return 属性名 + */ + @Link(annotation = Link.class, attribute = "attribute", type = RelationType.FORCE_ALIAS_FOR) + String attribute() default ""; + +} diff --git a/src/main/java/cn/hutool/core/annotation/MirrorLinkAnnotationPostProcessor.java b/src/main/java/cn/hutool/core/annotation/MirrorLinkAnnotationPostProcessor.java new file mode 100644 index 0000000..1fc0a20 --- /dev/null +++ b/src/main/java/cn/hutool/core/annotation/MirrorLinkAnnotationPostProcessor.java @@ -0,0 +1,132 @@ +package cn.hutool.core.annotation; + +import cn.hutool.core.lang.Assert; +import cn.hutool.core.text.CharSequenceUtil; +import cn.hutool.core.util.ObjectUtil; + +/** + *

用于处理注解对象中带有{@link Link}注解,且{@link Link#type()}为{@link RelationType#MIRROR_FOR}的属性。
+ * 当该处理器执行完毕后,原始合成注解中被{@link Link}注解的属性与{@link Link}注解指向的目标注解的属性, + * 都将会被被包装并替换为{@link MirroredAnnotationAttribute}。 + * + * @author huangchengxing + * @see RelationType#MIRROR_FOR + * @see MirroredAnnotationAttribute + */ +public class MirrorLinkAnnotationPostProcessor extends AbstractLinkAnnotationPostProcessor { + + private static final RelationType[] PROCESSED_RELATION_TYPES = new RelationType[]{ RelationType.MIRROR_FOR }; + + @Override + public int order() { + return Integer.MIN_VALUE + 1; + } + + /** + * 该处理器只处理{@link Link#type()}类型为{@link RelationType#MIRROR_FOR}的注解属性 + * + * @return 仅有{@link RelationType#MIRROR_FOR}数组 + */ + @Override + protected RelationType[] processTypes() { + return PROCESSED_RELATION_TYPES; + } + + /** + * 将存在镜像关系的合成注解属性分别包装为{@link MirroredAnnotationAttribute}对象, + * 并使用包装后{@link MirroredAnnotationAttribute}替换在它们对应合成注解实例中的{@link AnnotationAttribute} + * + * @param synthesizer 注解合成器 + * @param annotation {@code originalAttribute}上的{@link Link}注解对象 + * @param originalAnnotation 当前正在处理的{@link SynthesizedAnnotation}对象 + * @param originalAttribute {@code originalAnnotation}上的待处理的属性 + * @param linkedAnnotation {@link Link}指向的关联注解对象 + * @param linkedAttribute {@link Link}指向的{@code originalAnnotation}中的关联属性,该参数可能为空 + */ + @Override + protected void processLinkedAttribute( + AnnotationSynthesizer synthesizer, Link annotation, + SynthesizedAnnotation originalAnnotation, AnnotationAttribute originalAttribute, + SynthesizedAnnotation linkedAnnotation, AnnotationAttribute linkedAttribute) { + + // 镜像属性必然成对出现,因此此处必定存在三种情况: + // 1.两属性都不为镜像属性,此时继续进行后续处理; + // 2.两属性都为镜像属性,并且指向对方,此时无需后续处理; + // 3.两属性仅有任意一属性为镜像属性,此时镜像属性必然未指向当前原始属性,此时应该抛出异常; + if (originalAttribute instanceof MirroredAnnotationAttribute + || linkedAttribute instanceof MirroredAnnotationAttribute) { + checkMirrored(originalAttribute, linkedAttribute); + return; + } + + // 校验镜像关系 + checkMirrorRelation(annotation, originalAttribute, linkedAttribute); + // 包装这一对镜像属性,并替换原注解中的对应属性 + final AnnotationAttribute mirroredOriginalAttribute = new MirroredAnnotationAttribute(originalAttribute, linkedAttribute); + originalAnnotation.setAttribute(originalAttribute.getAttributeName(), mirroredOriginalAttribute); + final AnnotationAttribute mirroredTargetAttribute = new MirroredAnnotationAttribute(linkedAttribute, originalAttribute); + linkedAnnotation.setAttribute(annotation.attribute(), mirroredTargetAttribute); + } + + /** + * 检查映射关系是否正确 + */ + private void checkMirrored(AnnotationAttribute original, AnnotationAttribute mirror) { + final boolean originalAttributeMirrored = original instanceof MirroredAnnotationAttribute; + final boolean mirrorAttributeMirrored = mirror instanceof MirroredAnnotationAttribute; + + // 校验通过 + final boolean passed = originalAttributeMirrored && mirrorAttributeMirrored + && ObjectUtil.equals(((MirroredAnnotationAttribute)original).getLinked(), ((MirroredAnnotationAttribute)mirror).getOriginal()); + if (passed) { + return; + } + + // 校验失败,拼装异常信息用于抛出异常 + String errorMsg; + // 原始字段已经跟其他字段形成镜像 + if (originalAttributeMirrored && !mirrorAttributeMirrored) { + errorMsg = CharSequenceUtil.format( + "attribute [{}] cannot mirror for [{}], because it's already mirrored for [{}]", + original.getAttribute(), mirror.getAttribute(), ((MirroredAnnotationAttribute)original).getLinked() + ); + } + // 镜像字段已经跟其他字段形成镜像 + else if (!originalAttributeMirrored && mirrorAttributeMirrored) { + errorMsg = CharSequenceUtil.format( + "attribute [{}] cannot mirror for [{}], because it's already mirrored for [{}]", + mirror.getAttribute(), original.getAttribute(), ((MirroredAnnotationAttribute)mirror).getLinked() + ); + } + // 两者都形成了镜像,但是都未指向对方,理论上不会存在该情况 + else { + errorMsg = CharSequenceUtil.format( + "attribute [{}] cannot mirror for [{}], because [{}] already mirrored for [{}] and [{}] already mirrored for [{}]", + mirror.getAttribute(), original.getAttribute(), + mirror.getAttribute(), ((MirroredAnnotationAttribute)mirror).getLinked(), + original.getAttribute(), ((MirroredAnnotationAttribute)original).getLinked() + ); + } + + throw new IllegalArgumentException(errorMsg); + } + + /** + * 基本校验 + */ + private void checkMirrorRelation(Link annotation, AnnotationAttribute original, AnnotationAttribute mirror) { + // 镜像属性必须存在 + checkLinkedAttributeNotNull(original, mirror, annotation); + // 镜像属性返回值必须一致 + checkAttributeType(original, mirror); + // 镜像属性上必须存在对应的注解 + final Link mirrorAttributeAnnotation = getLinkAnnotation(mirror, RelationType.MIRROR_FOR); + Assert.isTrue( + ObjectUtil.isNotNull(mirrorAttributeAnnotation) && RelationType.MIRROR_FOR.equals(mirrorAttributeAnnotation.type()), + "mirror attribute [{}] of original attribute [{}] must marked by @Link, and also @LinkType.type() must is [{}]", + mirror.getAttribute(), original.getAttribute(), RelationType.MIRROR_FOR + ); + checkLinkedSelf(original, mirror); + } + +} diff --git a/src/main/java/cn/hutool/core/annotation/MirroredAnnotationAttribute.java b/src/main/java/cn/hutool/core/annotation/MirroredAnnotationAttribute.java new file mode 100644 index 0000000..6bb8000 --- /dev/null +++ b/src/main/java/cn/hutool/core/annotation/MirroredAnnotationAttribute.java @@ -0,0 +1,48 @@ +package cn.hutool.core.annotation; + +import cn.hutool.core.lang.Assert; + +/** + * 表示存在对应镜像属性的注解属性,当获取值时将根据{@link RelationType#MIRROR_FOR}的规则进行处理 + * + * @author huangchengxing + * @see MirrorLinkAnnotationPostProcessor + * @see RelationType#MIRROR_FOR + */ +public class MirroredAnnotationAttribute extends AbstractWrappedAnnotationAttribute { + + public MirroredAnnotationAttribute(AnnotationAttribute origin, AnnotationAttribute linked) { + super(origin, linked); + } + + @Override + public Object getValue() { + final boolean originIsDefault = original.isValueEquivalentToDefaultValue(); + final boolean targetIsDefault = linked.isValueEquivalentToDefaultValue(); + final Object originValue = original.getValue(); + final Object targetValue = linked.getValue(); + + // 都为默认值,或都为非默认值时,两方法的返回值必须相等 + if (originIsDefault == targetIsDefault) { + Assert.equals( + originValue, targetValue, + "the values of attributes [{}] and [{}] that mirror each other are different: [{}] <==> [{}]", + original.getAttribute(), linked.getAttribute(), originValue, targetValue + ); + return originValue; + } + + // 两者有一者不为默认值时,优先返回非默认值 + return originIsDefault ? targetValue : originValue; + } + + /** + * 当{@link #original}与{@link #linked}都为默认值时返回{@code true} + * + * @return 是否 + */ + @Override + public boolean isValueEquivalentToDefaultValue() { + return original.isValueEquivalentToDefaultValue() && linked.isValueEquivalentToDefaultValue(); + } +} diff --git a/src/main/java/cn/hutool/core/annotation/PropIgnore.java b/src/main/java/cn/hutool/core/annotation/PropIgnore.java new file mode 100644 index 0000000..cc3f6dc --- /dev/null +++ b/src/main/java/cn/hutool/core/annotation/PropIgnore.java @@ -0,0 +1,21 @@ +package cn.hutool.core.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * 属性忽略注解,使用此注解的字段等会被忽略,主要用于Bean拷贝、Bean转Map等
+ * 此注解应用于字段时,忽略读取和设置属性值,应用于setXXX方法忽略设置值,应用于getXXX忽略读取值 + * + * @author Looly + * @since 5.4.2 + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER}) +public @interface PropIgnore { + +} diff --git a/src/main/java/cn/hutool/core/annotation/RelationType.java b/src/main/java/cn/hutool/core/annotation/RelationType.java new file mode 100644 index 0000000..6d4c306 --- /dev/null +++ b/src/main/java/cn/hutool/core/annotation/RelationType.java @@ -0,0 +1,50 @@ +package cn.hutool.core.annotation; + +/** + *

注解属性的关系类型
+ * 若将被{@link Link}注解的属性称为“原始属性”,而在{@link Link}注解中指向的注解属性称为“关联属性”, + * 则该枚举用于描述“原始属性”与“关联属性”在{@link SynthesizedAggregateAnnotation}处理过程中的作用关系。
+ * 根据在{@link Link#type()}中指定的关系类型的不同,通过{@link SynthesizedAggregateAnnotation}合成的注解的属性值也将有所变化。 + * + *

当一个注解中的所有属性同时具备多种关系时,将依次按下述顺序处理: + *

    + *
  1. 属性上的{@link Alias}注解;
  2. + *
  3. 属性上的{@link Link}注解,且{@link Link#type()}为{@link #MIRROR_FOR};
  4. + *
  5. 属性上的{@link Link}注解,且{@link Link#type()}为{@link #FORCE_ALIAS_FOR};
  6. + *
  7. 属性上的{@link Link}注解,且{@link Link#type()}为{@link #ALIAS_FOR};
  8. + *
+ * + * @author huangchengxing + * @see SynthesizedAggregateAnnotation + * @see Link + */ +public enum RelationType { + + /** + *

表示注解的属性与指定的属性互为镜像,通过一个属性将能够获得对方的值。
+ * 它们遵循下述规则: + *

    + *
  • 互为镜像的两个属性,必须同时通过指定模式为{@code MIRROR_FOR}的{@link Link}注解指定对方;
  • + *
  • 互为镜像的两个属性,类型必须一致;
  • + *
  • 互为镜像的两个属性在获取值,且两者的值皆不同时,必须且仅允许有一个非默认值,该值被优先返回;
  • + *
  • 互为镜像的两个属性,在值都为默认值或都不为默认值时,两者的值必须相等;
  • + *
+ */ + MIRROR_FOR, + + /** + *

表示“原始属性”将作为“关联属性”的别名。 + *

    + *
  • 当“原始属性”为默认值时,获取“关联属性”将返回“关联属性”本身的值;
  • + *
  • 当“原始属性”不为默认值时,获取“关联属性”将返回“原始属性”的值;
  • + *
+ */ + ALIAS_FOR, + + /** + *

表示“原始属性”将强制作为“关联属性”的别名。效果等同于在“原始属性”上添加{@link Alias}注解, + * 任何情况下,获取“关联属性”的值都将直接返回“原始属性”的值 + */ + FORCE_ALIAS_FOR + +} diff --git a/src/main/java/cn/hutool/core/annotation/SynthesizedAggregateAnnotation.java b/src/main/java/cn/hutool/core/annotation/SynthesizedAggregateAnnotation.java new file mode 100644 index 0000000..e006f3f --- /dev/null +++ b/src/main/java/cn/hutool/core/annotation/SynthesizedAggregateAnnotation.java @@ -0,0 +1,102 @@ +package cn.hutool.core.annotation; + +import java.lang.annotation.Annotation; + +/** + *

表示基于特定规则聚合,将一组注解聚合而来的注解对象, + * 该注解对象允许根据一定规则“合成”一些跟原始注解属性不一样合成注解。 + * + *

合成注解一般被用于处理类层级结果中具有直接或间接关联的注解对象, + * 当实例被创建时,会获取到这些注解对象,并使用{@link SynthesizedAnnotationSelector}对类型相同的注解进行过滤, + * 并最终得到类型不重复的有效注解对象。这些有效注解将被包装为{@link SynthesizedAnnotation}, + * 然后最终用于“合成”一个{@link SynthesizedAggregateAnnotation}。
+ * {@link SynthesizedAnnotationSelector}是合成注解生命周期中的第一个钩子, + * 自定义选择器以拦截原始注解被扫描的过程。 + * + *

当合成注解完成对待合成注解的扫描,并完成了必要属性的加载后, + * 将会按顺序依次调用{@link SynthesizedAnnotationPostProcessor}, + * 注解后置处理器允许用于对完成注解的待合成注解进行二次调整, + * 该钩子一般用于根据{@link Link}注解对属性进行调整。
+ * {@link SynthesizedAnnotationPostProcessor}是合成注解生命周期中的第二个钩子, + * 自定义后置处理器以拦截原始在转为待合成注解后的初始化过程。 + * + *

合成注解允许通过{@link #synthesize(Class)}合成一个指定的注解对象, + * 该方法返回的注解对象可能是原始的注解对象,也有可能通过动态代理的方式生成, + * 该对象实例的属性不一定来自对象本身,而是来自于经过{@link SynthesizedAnnotationAttributeProcessor} + * 处理后的、用于合成当前实例的全部关联注解的相关属性。
+ * {@link SynthesizedAnnotationAttributeProcessor}是合成注解生命周期中的第三个钩子, + * 自定义属性处理器以拦截合成注解的取值过程。 + * + * @author huangchengxing + * @see AnnotationSynthesizer + * @see SynthesizedAnnotation + * @see SynthesizedAnnotationSelector + * @see SynthesizedAnnotationAttributeProcessor + * @see SynthesizedAnnotationPostProcessor + * @see GenericSynthesizedAggregateAnnotation + */ +public interface SynthesizedAggregateAnnotation extends AggregateAnnotation, Hierarchical, AnnotationSynthesizer, AnnotationAttributeValueProvider { + + // ================== hierarchical ================== + + /** + * 距离{@link #getRoot()}返回值的垂直距离, + * 默认聚合注解即为根对象,因此返回0 + * + * @return 距离{@link #getRoot()}返回值的水平距离, + */ + @Override + default int getVerticalDistance() { + return 0; + } + + /** + * 距离{@link #getRoot()}返回值的水平距离, + * 默认聚合注解即为根对象,因此返回0 + * + * @return 距离{@link #getRoot()}返回值的水平距离, + */ + @Override + default int getHorizontalDistance() { + return 0; + } + + // ================== synthesize ================== + + /** + * 获取在聚合中存在的指定注解对象 + * + * @param annotationType 注解类型 + * @param 注解类型 + * @return 注解对象 + */ + T getAnnotation(Class annotationType); + + /** + * 获取合成注解属性处理器 + * + * @return 合成注解属性处理器 + */ + SynthesizedAnnotationAttributeProcessor getAnnotationAttributeProcessor(); + + /** + * 获取当前的注解类型 + * + * @return 注解类型 + */ + @Override + default Class annotationType() { + return this.getClass(); + } + + /** + * 从聚合中获取指定类型的属性值 + * + * @param attributeName 属性名称 + * @param attributeType 属性类型 + * @return 属性值 + */ + @Override + Object getAttributeValue(String attributeName, Class attributeType); + +} diff --git a/src/main/java/cn/hutool/core/annotation/SynthesizedAnnotation.java b/src/main/java/cn/hutool/core/annotation/SynthesizedAnnotation.java new file mode 100644 index 0000000..4180599 --- /dev/null +++ b/src/main/java/cn/hutool/core/annotation/SynthesizedAnnotation.java @@ -0,0 +1,96 @@ +package cn.hutool.core.annotation; + +import cn.hutool.core.collection.CollUtil; + +import java.lang.annotation.Annotation; +import java.util.Map; +import java.util.function.UnaryOperator; + +/** + *

用于在{@link SynthesizedAggregateAnnotation}中表示一个处于合成状态的注解对象。
+ * 当对多个合成注解排序时,默认使用{@link #DEFAULT_HIERARCHICAL_COMPARATOR}进行排序, + * 从保证合成注解按{@link #getVerticalDistance()}与{@link #getHorizontalDistance()}的返回值保持有序, + * 从而使得距离根元素更接近的注解对象在被处理是具有更高的优先级。 + * + * @author huangchengxing + * @see SynthesizedAggregateAnnotation + */ +public interface SynthesizedAnnotation extends Annotation, Hierarchical, AnnotationAttributeValueProvider { + + /** + * 获取被合成的注解对象 + * + * @return 注解对象 + */ + Annotation getAnnotation(); + + /** + * 获取该合成注解与根对象的垂直距离。 + * 默认情况下,该距离即为当前注解与根对象之间相隔的层级数。 + * + * @return 合成注解与根对象的垂直距离 + */ + @Override + int getVerticalDistance(); + + /** + * 获取该合成注解与根对象的水平距离。 + * 默认情况下,该距离即为当前注解与根对象之间相隔的已经被扫描到的注解数。 + * + * @return 合成注解与根对象的水平距离 + */ + @Override + int getHorizontalDistance(); + + /** + * 注解是否存在该名称相同,且类型一致的属性 + * + * @param attributeName 属性名 + * @param returnType 返回值类型 + * @return 是否存在该属性 + */ + boolean hasAttribute(String attributeName, Class returnType); + + /** + * 获取该注解的全部属性 + * + * @return 注解属性 + */ + Map getAttributes(); + + /** + * 设置该注解的全部属性 + * + * @param attributes 注解属性 + */ + default void setAttributes(Map attributes) { + if (CollUtil.isNotEmpty(attributes)) { + attributes.forEach(this::setAttribute); + } + } + + /** + * 设置属性值 + * + * @param attributeName 属性名称 + * @param attribute 注解属性 + */ + void setAttribute(String attributeName, AnnotationAttribute attribute); + + /** + * 替换属性值 + * + * @param attributeName 属性名 + * @param operator 替换操作 + */ + void replaceAttribute(String attributeName, UnaryOperator operator); + + /** + * 获取属性值 + * + * @param attributeName 属性名 + * @return 属性值 + */ + Object getAttributeValue(String attributeName); + +} diff --git a/src/main/java/cn/hutool/core/annotation/SynthesizedAnnotationAttributeProcessor.java b/src/main/java/cn/hutool/core/annotation/SynthesizedAnnotationAttributeProcessor.java new file mode 100644 index 0000000..7a7debd --- /dev/null +++ b/src/main/java/cn/hutool/core/annotation/SynthesizedAnnotationAttributeProcessor.java @@ -0,0 +1,24 @@ +package cn.hutool.core.annotation; + +import java.util.Collection; + +/** + * 合成注解属性选择器。用于在{@link SynthesizedAggregateAnnotation}中从指定类型的合成注解里获取到对应的属性值 + * + * @author huangchengxing + */ +@FunctionalInterface +public interface SynthesizedAnnotationAttributeProcessor { + + /** + * 从一批被合成注解中,获取指定名称与类型的属性值 + * + * @param attributeName 属性名称 + * @param attributeType 属性类型 + * @param synthesizedAnnotations 被合成的注解 + * @param 属性类型 + * @return 属性值 + */ + R getAttributeValue(String attributeName, Class attributeType, Collection synthesizedAnnotations); + +} diff --git a/src/main/java/cn/hutool/core/annotation/SynthesizedAnnotationPostProcessor.java b/src/main/java/cn/hutool/core/annotation/SynthesizedAnnotationPostProcessor.java new file mode 100644 index 0000000..91e701b --- /dev/null +++ b/src/main/java/cn/hutool/core/annotation/SynthesizedAnnotationPostProcessor.java @@ -0,0 +1,71 @@ +package cn.hutool.core.annotation; + +import cn.hutool.core.comparator.CompareUtil; + +import java.util.Comparator; + +/** + *

被合成注解后置处理器,用于在{@link SynthesizedAggregateAnnotation}加载完所有待合成注解后, + * 再对加载好的{@link SynthesizedAnnotation}进行后置处理。
+ * 当多个{@link SynthesizedAnnotationPostProcessor}需要一起执行时,将按照{@link #order()}的返回值进行排序, + * 该值更小的处理器将被优先执行。 + * + *

该接口存在多个实现类,调用者应当保证在任何时候,对一批后置处理器的调用顺序都符合: + *

    + *
  • {@link AliasAnnotationPostProcessor};
  • + *
  • {@link MirrorLinkAnnotationPostProcessor};
  • + *
  • {@link AliasLinkAnnotationPostProcessor};
  • + *
  • 其他后置处理器;
  • + *
+ * + * @author huangchengxing + * @see AliasAnnotationPostProcessor + * @see MirrorLinkAnnotationPostProcessor + * @see AliasLinkAnnotationPostProcessor + */ +public interface SynthesizedAnnotationPostProcessor extends Comparable { + + /** + * 属性上带有{@link Alias}的注解对象的后置处理器 + */ + AliasAnnotationPostProcessor ALIAS_ANNOTATION_POST_PROCESSOR = new AliasAnnotationPostProcessor(); + + /** + * 属性上带有{@link Link},且与其他注解的属性存在镜像关系的注解对象的后置处理器 + */ + MirrorLinkAnnotationPostProcessor MIRROR_LINK_ANNOTATION_POST_PROCESSOR = new MirrorLinkAnnotationPostProcessor(); + + /** + * 属性上带有{@link Link},且与其他注解的属性存在别名关系的注解对象的后置处理器 + */ + AliasLinkAnnotationPostProcessor ALIAS_LINK_ANNOTATION_POST_PROCESSOR = new AliasLinkAnnotationPostProcessor(); + + /** + * 在一组后置处理器中被调用的顺序,越小越靠前 + * + * @return 排序值 + */ + default int order() { + return Integer.MAX_VALUE; + } + + /** + * 比较两个后置处理器的{@link #order()}返回值 + * + * @param o 比较对象 + * @return 大小 + */ + @Override + default int compareTo(SynthesizedAnnotationPostProcessor o) { + return CompareUtil.compare(this, o, Comparator.comparing(SynthesizedAnnotationPostProcessor::order)); + } + + /** + * 给定指定被合成注解与其所属的合成注解聚合器实例,经过处理后返回最终 + * + * @param synthesizedAnnotation 合成的注解 + * @param synthesizer 注解合成器 + */ + void process(SynthesizedAnnotation synthesizedAnnotation, AnnotationSynthesizer synthesizer); + +} diff --git a/src/main/java/cn/hutool/core/annotation/SynthesizedAnnotationProxy.java b/src/main/java/cn/hutool/core/annotation/SynthesizedAnnotationProxy.java new file mode 100644 index 0000000..233d849 --- /dev/null +++ b/src/main/java/cn/hutool/core/annotation/SynthesizedAnnotationProxy.java @@ -0,0 +1,160 @@ +package cn.hutool.core.annotation; + +import cn.hutool.core.lang.Assert; +import cn.hutool.core.lang.Opt; +import cn.hutool.core.text.CharSequenceUtil; +import cn.hutool.core.util.ClassUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.ReflectUtil; + +import java.lang.annotation.Annotation; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.function.BiFunction; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * 合成注解代理类,用于为{@link SynthesizedAnnotation}生成对应的合成注解代理对象 + * + * @author huangchengxing + * @see SynthesizedAnnotation + * @see AnnotationAttributeValueProvider + */ +public class SynthesizedAnnotationProxy implements InvocationHandler { + + private final AnnotationAttributeValueProvider annotationAttributeValueProvider; + private final SynthesizedAnnotation annotation; + private final Map> methods; + + /** + * 创建一个代理注解,生成的代理对象将是{@link SyntheticProxyAnnotation}与指定的注解类的子类。 + * + * @param 注解类型 + * @param annotationType 注解类型 + * @param annotationAttributeValueProvider 注解属性值获取器 + * @param annotation 合成注解 + * @return 代理注解 + */ + @SuppressWarnings("unchecked") + public static T create( + Class annotationType, + AnnotationAttributeValueProvider annotationAttributeValueProvider, + SynthesizedAnnotation annotation) { + if (ObjectUtil.isNull(annotation)) { + return null; + } + final SynthesizedAnnotationProxy proxyHandler = new SynthesizedAnnotationProxy(annotationAttributeValueProvider, annotation); + if (ObjectUtil.isNull(annotation)) { + return null; + } + return (T) Proxy.newProxyInstance( + annotationType.getClassLoader(), + new Class[]{annotationType, SyntheticProxyAnnotation.class}, + proxyHandler + ); + } + + /** + * 创建一个代理注解,生成的代理对象将是{@link SyntheticProxyAnnotation}与指定的注解类的子类。 + * + * @param 注解类型 + * @param annotationType 注解类型 + * @param annotation 合成注解 + * @return 代理注解 + */ + public static T create( + Class annotationType, SynthesizedAnnotation annotation) { + return create(annotationType, annotation, annotation); + } + + /** + * 该类是否为通过{@code SynthesizedAnnotationProxy}生成的代理类 + * + * @param annotationType 注解类型 + * @return 是否 + */ + public static boolean isProxyAnnotation(Class annotationType) { + return ClassUtil.isAssignable(SyntheticProxyAnnotation.class, annotationType); + } + + SynthesizedAnnotationProxy(AnnotationAttributeValueProvider annotationAttributeValueProvider, SynthesizedAnnotation annotation) { + Assert.notNull(annotationAttributeValueProvider, "annotationAttributeValueProvider must not null"); + Assert.notNull(annotation, "annotation must not null"); + this.annotationAttributeValueProvider = annotationAttributeValueProvider; + this.annotation = annotation; + this.methods = new HashMap<>(9); + loadMethods(); + } + + @Override + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + return Opt.ofNullable(methods.get(method.getName())) + .map(m -> m.apply(method, args)) + .orElseGet(() -> ReflectUtil.invoke(this, method, args)); + } + + // ========================= 代理方法 ========================= + + void loadMethods() { + methods.put("toString", (method, args) -> proxyToString()); + methods.put("hashCode", (method, args) -> proxyHashCode()); + methods.put("getSynthesizedAnnotation", (method, args) -> proxyGetSynthesizedAnnotation()); + methods.put("getRoot", (method, args) -> annotation.getRoot()); + methods.put("getVerticalDistance", (method, args) -> annotation.getVerticalDistance()); + methods.put("getHorizontalDistance", (method, args) -> annotation.getHorizontalDistance()); + methods.put("hasAttribute", (method, args) -> annotation.hasAttribute((String) args[0], (Class) args[1])); + methods.put("getAttributes", (method, args) -> annotation.getAttributes()); + methods.put("setAttribute", (method, args) -> { + throw new UnsupportedOperationException("proxied annotation can not reset attributes"); + }); + methods.put("getAttributeValue", (method, args) -> annotation.getAttributeValue((String) args[0])); + methods.put("annotationType", (method, args) -> annotation.annotationType()); + for (final Method declaredMethod : ClassUtil.getDeclaredMethods(annotation.getAnnotation().annotationType())) { + methods.put(declaredMethod.getName(), (method, args) -> proxyAttributeValue(method)); + } + } + + private String proxyToString() { + final String attributes = Stream.of(ClassUtil.getDeclaredMethods(annotation.getAnnotation().annotationType())) + .filter(AnnotationUtil::isAttributeMethod) + .map(method -> CharSequenceUtil.format( + "{}={}", method.getName(), proxyAttributeValue(method)) + ) + .collect(Collectors.joining(", ")); + return CharSequenceUtil.format("@{}({})", annotation.annotationType().getName(), attributes); + } + + private int proxyHashCode() { + return Objects.hash(annotationAttributeValueProvider, annotation); + } + + private Object proxyGetSynthesizedAnnotation() { + return annotation; + } + + private Object proxyAttributeValue(Method attributeMethod) { + return annotationAttributeValueProvider.getAttributeValue(attributeMethod.getName(), attributeMethod.getReturnType()); + } + + /** + * 通过代理类生成的合成注解 + * + * @author huangchengxing + */ + interface SyntheticProxyAnnotation extends SynthesizedAnnotation { + + /** + * 获取该代理注解对应的已合成注解 + * + * @return 理注解对应的已合成注解 + */ + SynthesizedAnnotation getSynthesizedAnnotation(); + + } + +} diff --git a/src/main/java/cn/hutool/core/annotation/SynthesizedAnnotationSelector.java b/src/main/java/cn/hutool/core/annotation/SynthesizedAnnotationSelector.java new file mode 100644 index 0000000..fa2b91e --- /dev/null +++ b/src/main/java/cn/hutool/core/annotation/SynthesizedAnnotationSelector.java @@ -0,0 +1,82 @@ +package cn.hutool.core.annotation; + +/** + * 注解选择器,指定两个注解,选择其中一个返回。
+ * 该接口用于在{@link SynthesizedAggregateAnnotation}中用于从一批相同的注解对象中筛选最终用于合成注解对象。 + * + * @author huangchengxing + */ +@FunctionalInterface +public interface SynthesizedAnnotationSelector { + + /** + * 返回距离根对象更近的注解,当距离一样时优先返回旧注解 + */ + SynthesizedAnnotationSelector NEAREST_AND_OLDEST_PRIORITY = new NearestAndOldestPrioritySelector(); + + /** + * 返回距离根对象更近的注解,当距离一样时优先返回新注解 + */ + SynthesizedAnnotationSelector NEAREST_AND_NEWEST_PRIORITY = new NearestAndNewestPrioritySelector(); + + /** + * 返回距离根对象更远的注解,当距离一样时优先返回旧注解 + */ + SynthesizedAnnotationSelector FARTHEST_AND_OLDEST_PRIORITY = new FarthestAndOldestPrioritySelector(); + + /** + * 返回距离根对象更远的注解,当距离一样时优先返回新注解 + */ + SynthesizedAnnotationSelector FARTHEST_AND_NEWEST_PRIORITY = new FarthestAndNewestPrioritySelector(); + + /** + * 比较两个被合成的注解,选择其中的一个并返回 + * + * @param 复合注解类型 + * @param oldAnnotation 已存在的注解,该参数不允许为空 + * @param newAnnotation 新获取的注解,该参数不允许为空 + * @return 被合成的注解 + */ + T choose(T oldAnnotation, T newAnnotation); + + /** + * 返回距离根对象更近的注解,当距离一样时优先返回旧注解 + */ + class NearestAndOldestPrioritySelector implements SynthesizedAnnotationSelector { + @Override + public T choose(T oldAnnotation, T newAnnotation) { + return Hierarchical.Selector.NEAREST_AND_OLDEST_PRIORITY.choose(oldAnnotation, newAnnotation); + } + } + + /** + * 返回距离根对象更近的注解,当距离一样时优先返回新注解 + */ + class NearestAndNewestPrioritySelector implements SynthesizedAnnotationSelector { + @Override + public T choose(T oldAnnotation, T newAnnotation) { + return Hierarchical.Selector.NEAREST_AND_NEWEST_PRIORITY.choose(oldAnnotation, newAnnotation); + } + } + + /** + * 返回距离根对象更远的注解,当距离一样时优先返回旧注解 + */ + class FarthestAndOldestPrioritySelector implements SynthesizedAnnotationSelector { + @Override + public T choose(T oldAnnotation, T newAnnotation) { + return Hierarchical.Selector.FARTHEST_AND_OLDEST_PRIORITY.choose(oldAnnotation, newAnnotation); + } + } + + /** + * 返回距离根对象更远的注解,当距离一样时优先返回新注解 + */ + class FarthestAndNewestPrioritySelector implements SynthesizedAnnotationSelector { + @Override + public T choose(T oldAnnotation, T newAnnotation) { + return Hierarchical.Selector.FARTHEST_AND_NEWEST_PRIORITY.choose(oldAnnotation, newAnnotation); + } + } + +} diff --git a/src/main/java/cn/hutool/core/annotation/WrappedAnnotationAttribute.java b/src/main/java/cn/hutool/core/annotation/WrappedAnnotationAttribute.java new file mode 100644 index 0000000..6ca5a3f --- /dev/null +++ b/src/main/java/cn/hutool/core/annotation/WrappedAnnotationAttribute.java @@ -0,0 +1,125 @@ +package cn.hutool.core.annotation; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; +import java.util.Collection; + +/** + *

表示一个被包装过的{@link AnnotationAttribute}, + * 该实例中的一些方法可能会被代理到另一个注解属性对象中, + * 从而使得通过原始的注解属性的方法获取到另一注解属性的值。
+ * 除了{@link #getValue()}以外,其他方法的返回值应当尽可能与{@link #getOriginal()} + * 返回的{@link AnnotationAttribute}对象的方法返回值一致。 + * + *

当包装类被包装了多层后,则规则生效优先级按包装的先后顺序倒序排序, + * 比如a、b互为镜像,此时a、b两属性应当都被{@link MirroredAnnotationAttribute}包装, + * 若再指定c为a的别名字段,则c、a、b都要在原基础上再次包装一层{@link AliasedAnnotationAttribute}。
+ * 此时a、b同时被包装了两层,则执行时,优先执行{@link AliasedAnnotationAttribute}的逻辑, + * 当该规则不生效时,比如c只有默认值,此时上一次的{@link MirroredAnnotationAttribute}的逻辑才会生效。 + * + *

被包装的{@link AnnotationAttribute}实际结构为一颗二叉树, + * 当包装类再次被包装时,实际上等于又添加了一个新的根节点, + * 此时需要同时更新树的全部关联叶子节点。 + * + * @author huangchengxing + * @see AnnotationAttribute + * @see ForceAliasedAnnotationAttribute + * @see AliasedAnnotationAttribute + * @see MirroredAnnotationAttribute + */ +public interface WrappedAnnotationAttribute extends AnnotationAttribute { + + // =========================== 新增方法 =========================== + + /** + * 获取被包装的{@link AnnotationAttribute}对象,该对象也可能是{@link AnnotationAttribute} + * + * @return 被包装的{@link AnnotationAttribute}对象 + */ + AnnotationAttribute getOriginal(); + + /** + * 获取最初的被包装的{@link AnnotationAttribute} + * + * @return 最初的被包装的{@link AnnotationAttribute} + */ + AnnotationAttribute getNonWrappedOriginal(); + + /** + * 获取包装{@link #getOriginal()}的{@link AnnotationAttribute}对象,该对象也可能是{@link AnnotationAttribute} + * + * @return 包装对象 + */ + AnnotationAttribute getLinked(); + + /** + * 遍历以当前实例为根节点的树结构,获取所有未被包装的属性 + * + * @return 叶子节点 + */ + Collection getAllLinkedNonWrappedAttributes(); + + // =========================== 代理实现 =========================== + + /** + * 获取注解对象 + * + * @return 注解对象 + */ + @Override + default Annotation getAnnotation() { + return getOriginal().getAnnotation(); + } + + /** + * 获取注解属性对应的方法 + * + * @return 注解属性对应的方法 + */ + @Override + default Method getAttribute() { + return getOriginal().getAttribute(); + } + + /** + * 该注解属性的值是否等于默认值
+ * 默认仅当{@link #getOriginal()}与{@link #getLinked()}返回的注解属性 + * 都为默认值时,才返回{@code true} + * + * @return 该注解属性的值是否等于默认值 + */ + @Override + boolean isValueEquivalentToDefaultValue(); + + /** + * 获取属性类型 + * + * @return 属性类型 + */ + @Override + default Class getAttributeType() { + return getOriginal().getAttributeType(); + } + + /** + * 获取属性上的注解 + * + * @param annotationType 注解类型 + * @return 注解对象 + */ + @Override + default T getAnnotation(Class annotationType) { + return getOriginal().getAnnotation(annotationType); + } + + /** + * 当前注解属性是否已经被{@link WrappedAnnotationAttribute}包装 + * + * @return boolean + */ + @Override + default boolean isWrapped() { + return true; + } + +} diff --git a/src/main/java/cn/hutool/core/annotation/package-info.java b/src/main/java/cn/hutool/core/annotation/package-info.java new file mode 100644 index 0000000..850654b --- /dev/null +++ b/src/main/java/cn/hutool/core/annotation/package-info.java @@ -0,0 +1,7 @@ +/** + * 注解包,提供增强型注解和注解工具类 + * + * @author looly + * + */ +package cn.hutool.core.annotation; \ No newline at end of file diff --git a/src/main/java/cn/hutool/core/annotation/scanner/AbstractTypeAnnotationScanner.java b/src/main/java/cn/hutool/core/annotation/scanner/AbstractTypeAnnotationScanner.java new file mode 100644 index 0000000..8a3b2e7 --- /dev/null +++ b/src/main/java/cn/hutool/core/annotation/scanner/AbstractTypeAnnotationScanner.java @@ -0,0 +1,288 @@ +package cn.hutool.core.annotation.scanner; + +import cn.hutool.core.annotation.AnnotationUtil; +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.ObjectUtil; + +import java.lang.annotation.Annotation; +import java.lang.reflect.AnnotatedElement; +import java.lang.reflect.Proxy; +import java.util.*; +import java.util.function.BiConsumer; +import java.util.function.Predicate; +import java.util.function.UnaryOperator; + +/** + * 为需要从类的层级结构中获取注解的{@link AnnotationScanner}提供基本实现 + * + * @author huangchengxing + */ +public abstract class AbstractTypeAnnotationScanner> implements AnnotationScanner { + + /** + * 是否允许扫描父类 + */ + private boolean includeSuperClass; + + /** + * 是否允许扫描父接口 + */ + private boolean includeInterfaces; + + /** + * 过滤器,若类型无法通过该过滤器,则该类型及其树结构将直接不被查找 + */ + private Predicate> filter; + + /** + * 排除的类型,以上类型及其树结构将直接不被查找 + */ + private final Set> excludeTypes; + + /** + * 转换器 + */ + private final List>> converters; + + /** + * 是否有转换器 + */ + private boolean hasConverters; + + /** + * 当前实例 + */ + private final T typedThis; + + /** + * 构造一个类注解扫描器 + * + * @param includeSuperClass 是否允许扫描父类 + * @param includeInterfaces 是否允许扫描父接口 + * @param filter 过滤器 + * @param excludeTypes 不包含的类型 + */ + @SuppressWarnings("unchecked") + protected AbstractTypeAnnotationScanner(boolean includeSuperClass, boolean includeInterfaces, Predicate> filter, Set> excludeTypes) { + Assert.notNull(filter, "filter must not null"); + Assert.notNull(excludeTypes, "excludeTypes must not null"); + this.includeSuperClass = includeSuperClass; + this.includeInterfaces = includeInterfaces; + this.filter = filter; + this.excludeTypes = excludeTypes; + this.converters = new ArrayList<>(); + this.typedThis = (T) this; + } + + /** + * 是否允许扫描父类 + * + * @return 是否允许扫描父类 + */ + public boolean isIncludeSuperClass() { + return includeSuperClass; + } + + /** + * 是否允许扫描父接口 + * + * @return 是否允许扫描父接口 + */ + public boolean isIncludeInterfaces() { + return includeInterfaces; + } + + /** + * 设置过滤器,若类型无法通过该过滤器,则该类型及其树结构将直接不被查找 + * + * @param filter 过滤器 + * @return 当前实例 + */ + public T setFilter(Predicate> filter) { + Assert.notNull(filter, "filter must not null"); + this.filter = filter; + return typedThis; + } + + /** + * 添加不扫描的类型,该类型及其树结构将直接不被查找 + * + * @param excludeTypes 不扫描的类型 + * @return 当前实例 + */ + public T addExcludeTypes(Class... excludeTypes) { + CollUtil.addAll(this.excludeTypes, excludeTypes); + return typedThis; + } + + /** + * 添加转换器 + * + * @param converter 转换器 + * @return 当前实例 + * @see JdkProxyClassConverter + */ + public T addConverters(UnaryOperator> converter) { + Assert.notNull(converter, "converter must not null"); + this.converters.add(converter); + if (!this.hasConverters) { + this.hasConverters = CollUtil.isNotEmpty(this.converters); + } + return typedThis; + } + + /** + * 是否允许扫描父类 + * + * @param includeSuperClass 是否 + * @return 当前实例 + */ + protected T setIncludeSuperClass(boolean includeSuperClass) { + this.includeSuperClass = includeSuperClass; + return typedThis; + } + + /** + * 是否允许扫描父接口 + * + * @param includeInterfaces 是否 + * @return 当前实例 + */ + protected T setIncludeInterfaces(boolean includeInterfaces) { + this.includeInterfaces = includeInterfaces; + return typedThis; + } + + /** + * 则根据广度优先递归扫描类的层级结构,并对层级结构中类/接口声明的层级索引和它们声明的注解对象进行处理 + * + * @param consumer 对获取到的注解和注解对应的层级索引的处理 + * @param annotatedEle 注解元素 + * @param filter 注解过滤器,无法通过过滤器的注解不会被处理。该参数允许为空。 + */ + @Override + public void scan(BiConsumer consumer, AnnotatedElement annotatedEle, Predicate filter) { + filter = ObjectUtil.defaultIfNull(filter, a -> annotation -> true); + final Class sourceClass = getClassFormAnnotatedElement(annotatedEle); + final Deque>> classDeque = CollUtil.newLinkedList(CollUtil.newArrayList(sourceClass)); + final Set> accessedTypes = new LinkedHashSet<>(); + int index = 0; + while (!classDeque.isEmpty()) { + final List> currClassQueue = classDeque.removeFirst(); + final List> nextClassQueue = new ArrayList<>(); + for (Class targetClass : currClassQueue) { + targetClass = convert(targetClass); + // 过滤不需要处理的类 + if (isNotNeedProcess(accessedTypes, targetClass)) { + continue; + } + accessedTypes.add(targetClass); + // 扫描父类 + scanSuperClassIfNecessary(nextClassQueue, targetClass); + // 扫描接口 + scanInterfaceIfNecessary(nextClassQueue, targetClass); + // 处理层级索引和注解 + final Annotation[] targetAnnotations = getAnnotationsFromTargetClass(annotatedEle, index, targetClass); + for (final Annotation annotation : targetAnnotations) { + if (AnnotationUtil.isNotJdkMateAnnotation(annotation.annotationType()) && filter.test(annotation)) { + consumer.accept(index, annotation); + } + } + index++; + } + if (CollUtil.isNotEmpty(nextClassQueue)) { + classDeque.addLast(nextClassQueue); + } + } + } + + /** + * 从要搜索的注解元素上获得要递归的类型 + * + * @param annotatedElement 注解元素 + * @return 要递归的类型 + */ + protected abstract Class getClassFormAnnotatedElement(AnnotatedElement annotatedElement); + + /** + * 从类上获取最终所需的目标注解 + * + * @param source 最初的注解元素 + * @param index 类的层级索引 + * @param targetClass 类 + * @return 最终所需的目标注解 + */ + protected abstract Annotation[] getAnnotationsFromTargetClass(AnnotatedElement source, int index, Class targetClass); + + /** + * 当前类是否不需要处理 + * + * @param accessedTypes 访问类型 + * @param targetClass 目标类型 + * @return 是否不需要处理 + */ + protected boolean isNotNeedProcess(Set> accessedTypes, Class targetClass) { + return ObjectUtil.isNull(targetClass) + || accessedTypes.contains(targetClass) + || excludeTypes.contains(targetClass) + || filter.negate().test(targetClass); + } + + /** + * 若{@link #includeInterfaces}为{@code true},则将目标类的父接口也添加到nextClasses + * + * @param nextClasses 下一个类集合 + * @param targetClass 目标类型 + */ + protected void scanInterfaceIfNecessary(List> nextClasses, Class targetClass) { + if (includeInterfaces) { + final Class[] interfaces = targetClass.getInterfaces(); + if (ArrayUtil.isNotEmpty(interfaces)) { + CollUtil.addAll(nextClasses, interfaces); + } + } + } + + /** + * 若{@link #includeSuperClass}为{@code true},则将目标类的父类也添加到nextClasses + * + * @param nextClassQueue 下一个类队列 + * @param targetClass 目标类型 + */ + protected void scanSuperClassIfNecessary(List> nextClassQueue, Class targetClass) { + if (includeSuperClass) { + final Class superClass = targetClass.getSuperclass(); + if (!ObjectUtil.equals(superClass, Object.class) && ObjectUtil.isNotNull(superClass)) { + nextClassQueue.add(superClass); + } + } + } + + /** + * 若存在转换器,则使用转换器对目标类进行转换 + * + * @param target 目标类 + * @return 转换后的类 + */ + protected Class convert(Class target) { + if (hasConverters) { + for (final UnaryOperator> converter : converters) { + target = converter.apply(target); + } + } + return target; + } + + /** + * 若类型为jdk代理类,则尝试转换为原始被代理类 + */ + public static class JdkProxyClassConverter implements UnaryOperator> { + @Override + public Class apply(Class sourceClass) { + return Proxy.isProxyClass(sourceClass) ? apply(sourceClass.getSuperclass()) : sourceClass; + } + } + +} diff --git a/src/main/java/cn/hutool/core/annotation/scanner/AnnotationScanner.java b/src/main/java/cn/hutool/core/annotation/scanner/AnnotationScanner.java new file mode 100644 index 0000000..dc108cb --- /dev/null +++ b/src/main/java/cn/hutool/core/annotation/scanner/AnnotationScanner.java @@ -0,0 +1,198 @@ +package cn.hutool.core.annotation.scanner; + +import cn.hutool.core.annotation.AnnotationUtil; +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.ObjectUtil; + +import java.lang.annotation.Annotation; +import java.lang.annotation.Inherited; +import java.lang.reflect.AnnotatedElement; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.function.BiConsumer; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + *

注解扫描器,用于从支持的可注解元素上获取所需注解 + * + *

默认提供了以下扫描方式: + *

    + *
  • {@link #NOTHING}:什么都不做,什么注解都不扫描;
  • + *
  • {@link #DIRECTLY}:扫描元素本身直接声明的注解,包括父类带有{@link Inherited}、被传递到元素上的注解;
  • + *
  • + * {@link #DIRECTLY_AND_META_ANNOTATION}:扫描元素本身直接声明的注解,包括父类带有{@link Inherited}、被传递到元素上的注解, + * 以及这些注解的元注解; + *
  • + *
  • {@link #SUPERCLASS}:扫描元素本身以及父类的层级结构中声明的注解;
  • + *
  • {@link #SUPERCLASS_AND_META_ANNOTATION}:扫描元素本身以及父类的层级结构中声明的注解,以及这些注解的元注解;
  • + *
  • {@link #INTERFACE}:扫描元素本身以及父接口的层级结构中声明的注解;
  • + *
  • {@link #INTERFACE_AND_META_ANNOTATION}:扫描元素本身以及父接口的层级结构中声明的注解,以及这些注解的元注解;
  • + *
  • {@link #TYPE_HIERARCHY}:扫描元素本身以及父类、父接口的层级结构中声明的注解;
  • + *
  • {@link #TYPE_HIERARCHY_AND_META_ANNOTATION}:扫描元素本身以及父接口、父接口的层级结构中声明的注解,以及这些注解的元注解;
  • + *
+ * + * @author huangchengxing + * @see TypeAnnotationScanner + * @see MethodAnnotationScanner + * @see FieldAnnotationScanner + * @see MetaAnnotationScanner + * @see ElementAnnotationScanner + * @see GenericAnnotationScanner + */ +public interface AnnotationScanner { + + // ============================ 预置的扫描器实例 ============================ + + /** + * 不扫描任何注解 + */ + AnnotationScanner NOTHING = new EmptyAnnotationScanner(); + + /** + * 扫描元素本身直接声明的注解,包括父类带有{@link Inherited}、被传递到元素上的注解的扫描器 + */ + AnnotationScanner DIRECTLY = new GenericAnnotationScanner(false, false, false); + + /** + * 扫描元素本身直接声明的注解,包括父类带有{@link Inherited}、被传递到元素上的注解,以及这些注解的元注解的扫描器 + */ + AnnotationScanner DIRECTLY_AND_META_ANNOTATION = new GenericAnnotationScanner(true, false, false); + + /** + * 扫描元素本身以及父类的层级结构中声明的注解的扫描器 + */ + AnnotationScanner SUPERCLASS = new GenericAnnotationScanner(false, true, false); + + /** + * 扫描元素本身以及父类的层级结构中声明的注解,以及这些注解的元注解的扫描器 + */ + AnnotationScanner SUPERCLASS_AND_META_ANNOTATION = new GenericAnnotationScanner(true, true, false); + + /** + * 扫描元素本身以及父接口的层级结构中声明的注解的扫描器 + */ + AnnotationScanner INTERFACE = new GenericAnnotationScanner(false, false, true); + + /** + * 扫描元素本身以及父接口的层级结构中声明的注解,以及这些注解的元注解的扫描器 + */ + AnnotationScanner INTERFACE_AND_META_ANNOTATION = new GenericAnnotationScanner(true, false, true); + + /** + * 扫描元素本身以及父类、父接口的层级结构中声明的注解的扫描器 + */ + AnnotationScanner TYPE_HIERARCHY = new GenericAnnotationScanner(false, true, true); + + /** + * 扫描元素本身以及父接口、父接口的层级结构中声明的注解,以及这些注解的元注解的扫描器 + */ + AnnotationScanner TYPE_HIERARCHY_AND_META_ANNOTATION = new GenericAnnotationScanner(true, true, true); + + // ============================ 静态方法 ============================ + + /** + * 给定一组扫描器,使用第一个支持处理该类型元素的扫描器获取元素上可能存在的注解 + * + * @param annotatedEle {@link AnnotatedElement},可以是Class、Method、Field、Constructor、ReflectPermission + * @param scanners 注解扫描器 + * @return 注解 + */ + static List scanByAnySupported(AnnotatedElement annotatedEle, AnnotationScanner... scanners) { + if (ObjectUtil.isNull(annotatedEle) && ArrayUtil.isNotEmpty(scanners)) { + return Collections.emptyList(); + } + return Stream.of(scanners) + .filter(scanner -> scanner.support(annotatedEle)) + .findFirst() + .map(scanner -> scanner.getAnnotations(annotatedEle)) + .orElseGet(Collections::emptyList); + } + + /** + * 根据指定的扫描器,扫描元素上可能存在的注解 + * + * @param annotatedEle {@link AnnotatedElement},可以是Class、Method、Field、Constructor、ReflectPermission + * @param scanners 注解扫描器 + * @return 注解 + */ + static List scanByAllSupported(AnnotatedElement annotatedEle, AnnotationScanner... scanners) { + if (ObjectUtil.isNull(annotatedEle) && ArrayUtil.isNotEmpty(scanners)) { + return Collections.emptyList(); + } + return Stream.of(scanners) + .map(scanner -> scanner.getAnnotationsIfSupport(annotatedEle)) + .flatMap(Collection::stream) + .collect(Collectors.toList()); + } + + // ============================ 抽象方法 ============================ + + /** + * 判断是否支持扫描该注解元素 + * + * @param annotatedEle {@link AnnotatedElement},可以是Class、Method、Field、Constructor、ReflectPermission + * @return 是否支持扫描该注解元素 + */ + default boolean support(AnnotatedElement annotatedEle) { + return false; + } + + /** + * 获取注解元素上的全部注解。调用该方法前,需要确保调用{@link #support(AnnotatedElement)}返回为true + * + * @param annotatedEle {@link AnnotatedElement},可以是Class、Method、Field、Constructor、ReflectPermission + * @return 注解 + */ + default List getAnnotations(AnnotatedElement annotatedEle) { + final List annotations = new ArrayList<>(); + scan((index, annotation) -> annotations.add(annotation), annotatedEle, null); + return annotations; + } + + /** + * 若{@link #support(AnnotatedElement)}返回{@code true}, + * 则调用并返回{@link #getAnnotations(AnnotatedElement)}结果, + * 否则返回{@link Collections#emptyList()} + * + * @param annotatedEle {@link AnnotatedElement},可以是Class、Method、Field、Constructor、ReflectPermission + * @return 注解 + */ + default List getAnnotationsIfSupport(AnnotatedElement annotatedEle) { + return support(annotatedEle) ? getAnnotations(annotatedEle) : Collections.emptyList(); + } + + /** + * 扫描注解元素的层级结构(若存在),然后对获取到的注解和注解对应的层级索引进行处理。 + * 调用该方法前,需要确保调用{@link #support(AnnotatedElement)}返回为true + * + * @param consumer 对获取到的注解和注解对应的层级索引的处理 + * @param annotatedEle {@link AnnotatedElement},可以是Class、Method、Field、Constructor、ReflectPermission + * @param filter 注解过滤器,无法通过过滤器的注解不会被处理。该参数允许为空。 + */ + default void scan(BiConsumer consumer, AnnotatedElement annotatedEle, Predicate filter) { + filter = ObjectUtil.defaultIfNull(filter, (a)->annotation -> true); + for (final Annotation annotation : annotatedEle.getAnnotations()) { + if (AnnotationUtil.isNotJdkMateAnnotation(annotation.annotationType()) && filter.test(annotation)) { + consumer.accept(0, annotation); + } + } + } + + /** + * 若{@link #support(AnnotatedElement)}返回{@code true},则调用{@link #scan(BiConsumer, AnnotatedElement, Predicate)} + * + * @param consumer 对获取到的注解和注解对应的层级索引的处理 + * @param annotatedEle {@link AnnotatedElement},可以是Class、Method、Field、Constructor、ReflectPermission + * @param filter 注解过滤器,无法通过过滤器的注解不会被处理。该参数允许为空。 + */ + default void scanIfSupport(BiConsumer consumer, AnnotatedElement annotatedEle, Predicate filter) { + if (support(annotatedEle)) { + scan(consumer, annotatedEle, filter); + } + } + +} diff --git a/src/main/java/cn/hutool/core/annotation/scanner/ElementAnnotationScanner.java b/src/main/java/cn/hutool/core/annotation/scanner/ElementAnnotationScanner.java new file mode 100644 index 0000000..88b22bc --- /dev/null +++ b/src/main/java/cn/hutool/core/annotation/scanner/ElementAnnotationScanner.java @@ -0,0 +1,44 @@ +package cn.hutool.core.annotation.scanner; + +import cn.hutool.core.util.ObjectUtil; + +import java.lang.annotation.Annotation; +import java.lang.reflect.AnnotatedElement; +import java.util.function.BiConsumer; +import java.util.function.Predicate; +import java.util.stream.Stream; + +/** + * 扫描{@link AnnotatedElement}上的注解,不支持处理层级对象 + * + * @author huangchengxing + */ +public class ElementAnnotationScanner implements AnnotationScanner { + + /** + * 判断是否支持扫描该注解元素,仅当注解元素不为空时返回{@code true} + * + * @param annotatedEle {@link AnnotatedElement},可以是Class、Method、Field、Constructor、ReflectPermission + * @return 是否支持扫描该注解元素 + */ + @Override + public boolean support(AnnotatedElement annotatedEle) { + return ObjectUtil.isNotNull(annotatedEle); + } + + /** + * 扫描{@link AnnotatedElement}上直接声明的注解,调用前需要确保调用{@link #support(AnnotatedElement)}返回为true + * + * @param consumer 对获取到的注解和注解对应的层级索引的处理 + * @param annotatedEle {@link AnnotatedElement},可以是Class、Method、Field、Constructor、ReflectPermission + * @param filter 注解过滤器,无法通过过滤器的注解不会被处理。该参数允许为空。 + */ + @Override + public void scan(BiConsumer consumer, AnnotatedElement annotatedEle, Predicate filter) { + filter = ObjectUtil.defaultIfNull(filter,a-> t -> true); + Stream.of(annotatedEle.getAnnotations()) + .filter(filter) + .forEach(annotation -> consumer.accept(0, annotation)); + } + +} diff --git a/src/main/java/cn/hutool/core/annotation/scanner/EmptyAnnotationScanner.java b/src/main/java/cn/hutool/core/annotation/scanner/EmptyAnnotationScanner.java new file mode 100644 index 0000000..972d1d7 --- /dev/null +++ b/src/main/java/cn/hutool/core/annotation/scanner/EmptyAnnotationScanner.java @@ -0,0 +1,31 @@ +package cn.hutool.core.annotation.scanner; + +import java.lang.annotation.Annotation; +import java.lang.reflect.AnnotatedElement; +import java.util.Collections; +import java.util.List; +import java.util.function.BiConsumer; +import java.util.function.Predicate; + +/** + * 默认不扫描任何元素的扫描器 + * + * @author huangchengxing + */ +public class EmptyAnnotationScanner implements AnnotationScanner { + + @Override + public boolean support(AnnotatedElement annotatedEle) { + return true; + } + + @Override + public List getAnnotations(AnnotatedElement annotatedEle) { + return Collections.emptyList(); + } + + @Override + public void scan(BiConsumer consumer, AnnotatedElement annotatedEle, Predicate filter) { + // do nothing + } +} diff --git a/src/main/java/cn/hutool/core/annotation/scanner/FieldAnnotationScanner.java b/src/main/java/cn/hutool/core/annotation/scanner/FieldAnnotationScanner.java new file mode 100644 index 0000000..e7d87cd --- /dev/null +++ b/src/main/java/cn/hutool/core/annotation/scanner/FieldAnnotationScanner.java @@ -0,0 +1,47 @@ +package cn.hutool.core.annotation.scanner; + +import cn.hutool.core.annotation.AnnotationUtil; +import cn.hutool.core.util.ObjectUtil; + +import java.lang.annotation.Annotation; +import java.lang.reflect.AnnotatedElement; +import java.lang.reflect.Field; +import java.util.function.BiConsumer; +import java.util.function.Predicate; + +/** + * 扫描{@link Field}上的注解 + * + * @author huangchengxing + */ +public class FieldAnnotationScanner implements AnnotationScanner { + + /** + * 判断是否支持扫描该注解元素,仅当注解元素是{@link Field}时返回{@code true} + * + * @param annotatedEle {@link AnnotatedElement},可以是Class、Method、Field、Constructor、ReflectPermission + * @return 是否支持扫描该注解元素 + */ + @Override + public boolean support(AnnotatedElement annotatedEle) { + return annotatedEle instanceof Field; + } + + /** + * 扫描{@link Field}上直接声明的注解,调用前需要确保调用{@link #support(AnnotatedElement)}返回为true + * + * @param consumer 对获取到的注解和注解对应的层级索引的处理 + * @param annotatedEle {@link AnnotatedElement},可以是Class、Method、Field、Constructor、ReflectPermission + * @param filter 注解过滤器,无法通过过滤器的注解不会被处理。该参数允许为空。 + */ + @Override + public void scan(BiConsumer consumer, AnnotatedElement annotatedEle, Predicate filter) { + filter = ObjectUtil.defaultIfNull(filter, a -> annotation -> true); + for (final Annotation annotation : annotatedEle.getAnnotations()) { + if (AnnotationUtil.isNotJdkMateAnnotation(annotation.annotationType()) && filter.test(annotation)) { + consumer.accept(0, annotation); + } + } + } + +} diff --git a/src/main/java/cn/hutool/core/annotation/scanner/GenericAnnotationScanner.java b/src/main/java/cn/hutool/core/annotation/scanner/GenericAnnotationScanner.java new file mode 100644 index 0000000..fe40f10 --- /dev/null +++ b/src/main/java/cn/hutool/core/annotation/scanner/GenericAnnotationScanner.java @@ -0,0 +1,149 @@ +package cn.hutool.core.annotation.scanner; + +import cn.hutool.core.map.multi.ListValueMap; +import cn.hutool.core.util.ObjectUtil; + +import java.lang.annotation.Annotation; +import java.lang.reflect.AnnotatedElement; +import java.lang.reflect.Method; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.function.BiConsumer; +import java.util.function.Predicate; + +/** + *

通用注解扫描器,支持按不同的层级结构扫描{@link AnnotatedElement}上的注解。 + * + *

当{@link AnnotatedElement}类型不同时,“层级结构”指向的对象将有所区别: + *

    + *
  • + * 当元素为{@link Method}时,此处层级结构指声明方法的类的层级结构, + * 扫描器将从层级结构中寻找与该方法签名相同的方法,并对其进行扫描; + *
  • + *
  • + * 当元素为{@link Class}时,此处层级结构即指类本身与其父类、父接口共同构成的层级结构, + * 扫描器将扫描层级结构中类、接口声明的注解; + *
  • + *
  • 当元素不为{@link Method}或{@link Class}时,则其层级结构仅有其本身一层;
  • + *
+ * 此外,扫描器支持在获取到层级结构中的注解对象后,再对注解对象的元注解进行扫描。 + * + * @author huangchengxing + * @see TypeAnnotationScanner + * @see MethodAnnotationScanner + * @see MetaAnnotationScanner + * @see ElementAnnotationScanner + */ +public class GenericAnnotationScanner implements AnnotationScanner { + + /** + * 类型扫描器 + */ + private final AnnotationScanner typeScanner; + + /** + * 方法扫描器 + */ + private final AnnotationScanner methodScanner; + + /** + * 元注解扫描器 + */ + private final AnnotationScanner metaScanner; + + /** + * 普通元素扫描器 + */ + private final AnnotationScanner elementScanner; + + /** + * 通用注解扫描器支持扫描所有类型的{@link AnnotatedElement} + * + * @param annotatedEle {@link AnnotatedElement},可以是Class、Method、Field、Constructor、ReflectPermission + * @return 是否支持扫描该注解元素 + */ + @Override + public boolean support(AnnotatedElement annotatedEle) { + return true; + } + + /** + * 构造一个通用注解扫描器 + * + * @param enableScanMetaAnnotation 是否扫描注解上的元注解 + * @param enableScanSupperClass 是否扫描父类 + * @param enableScanSupperInterface 是否扫描父接口 + */ + public GenericAnnotationScanner( + boolean enableScanMetaAnnotation, + boolean enableScanSupperClass, + boolean enableScanSupperInterface) { + + this.metaScanner = enableScanMetaAnnotation ? new MetaAnnotationScanner() : new EmptyAnnotationScanner(); + this.typeScanner = new TypeAnnotationScanner( + enableScanSupperClass, enableScanSupperInterface, a -> true, Collections.emptySet() + ); + this.methodScanner = new MethodAnnotationScanner( + enableScanSupperClass, enableScanSupperInterface, a -> true, Collections.emptySet() + ); + this.elementScanner = new ElementAnnotationScanner(); + } + + /** + * 扫描注解元素的层级结构(若存在),然后对获取到的注解和注解对应的层级索引进行处理 + * + * @param consumer 对获取到的注解和注解对应的层级索引的处理 + * @param annotatedEle {@link AnnotatedElement},可以是Class、Method、Field、Constructor、ReflectPermission + * @param filter 注解过滤器,无法通过过滤器的注解不会被处理。该参数允许为空。 + */ + @Override + public void scan(BiConsumer consumer, AnnotatedElement annotatedEle, Predicate filter) { + filter = ObjectUtil.defaultIfNull(filter, a -> t -> true); + if (ObjectUtil.isNull(annotatedEle)) { + return; + } + // 注解元素是类 + if (annotatedEle instanceof Class) { + scanElements(typeScanner, consumer, annotatedEle, filter); + } + // 注解元素是方法 + else if (annotatedEle instanceof Method) { + scanElements(methodScanner, consumer, annotatedEle, filter); + } + // 注解元素是其他类型 + else { + scanElements(elementScanner, consumer, annotatedEle, filter); + } + } + + /** + * 扫描注解类的层级结构(若存在),然后对获取到的注解和注解对应的层级索引进行处理 + * + * @param scanner 使用的扫描器 + * @param consumer 对获取到的注解和注解对应的层级索引的处理 + * @param annotatedEle {@link AnnotatedElement},可以是Class、Method、Field、Constructor、ReflectPermission + * @param filter 注解过滤器,无法通过过滤器的注解不会被处理。该参数允许为空。 + */ + private void scanElements( + AnnotationScanner scanner, + BiConsumer consumer, + AnnotatedElement annotatedEle, + Predicate filter) { + // 扫描类上注解 + final ListValueMap classAnnotations = new ListValueMap<>(new LinkedHashMap<>()); + scanner.scan((index, annotation) -> { + if (filter.test(annotation)) { + classAnnotations.putValue(index, annotation); + } + }, annotatedEle, filter); + + // 扫描元注解 + classAnnotations.forEach((index, annotations) -> + annotations.forEach(annotation -> { + consumer.accept(index, annotation); + metaScanner.scan(consumer, annotation.annotationType(), filter); + }) + ); + } + +} diff --git a/src/main/java/cn/hutool/core/annotation/scanner/MetaAnnotationScanner.java b/src/main/java/cn/hutool/core/annotation/scanner/MetaAnnotationScanner.java new file mode 100644 index 0000000..a054232 --- /dev/null +++ b/src/main/java/cn/hutool/core/annotation/scanner/MetaAnnotationScanner.java @@ -0,0 +1,110 @@ +package cn.hutool.core.annotation.scanner; + +import cn.hutool.core.annotation.AnnotationUtil; +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ClassUtil; +import cn.hutool.core.util.ObjectUtil; + +import java.lang.annotation.Annotation; +import java.lang.reflect.AnnotatedElement; +import java.util.*; +import java.util.function.BiConsumer; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * 扫描注解类上存在的注解,支持处理枚举实例或枚举类型 + * 需要注意,当待解析是枚举类时,有可能与{@link TypeAnnotationScanner}冲突 + * + * @author huangchengxing + * @see TypeAnnotationScanner + */ +public class MetaAnnotationScanner implements AnnotationScanner { + + /** + * 获取当前注解的元注解后,是否继续递归扫描的元注解的元注解 + */ + private final boolean includeSupperMetaAnnotation; + + /** + * 构造一个元注解扫描器 + * + * @param includeSupperMetaAnnotation 获取当前注解的元注解后,是否继续递归扫描的元注解的元注解 + */ + public MetaAnnotationScanner(boolean includeSupperMetaAnnotation) { + this.includeSupperMetaAnnotation = includeSupperMetaAnnotation; + } + + /** + * 构造一个元注解扫描器,默认在扫描当前注解上的元注解后,并继续递归扫描元注解 + */ + public MetaAnnotationScanner() { + this(true); + } + + /** + * 判断是否支持扫描该注解元素,仅当注解元素是{@link Annotation}接口的子类{@link Class}时返回{@code true} + * + * @param annotatedEle {@link AnnotatedElement},可以是Class、Method、Field、Constructor、ReflectPermission + * @return 是否支持扫描该注解元素 + */ + @Override + public boolean support(AnnotatedElement annotatedEle) { + return (annotatedEle instanceof Class && ClassUtil.isAssignable(Annotation.class, (Class) annotatedEle)); + } + + /** + * 获取注解元素上的全部注解。调用该方法前,需要确保调用{@link #support(AnnotatedElement)}返回为true + * + * @param annotatedEle {@link AnnotatedElement},可以是Class、Method、Field、Constructor、ReflectPermission + * @return 注解 + */ + @Override + public List getAnnotations(AnnotatedElement annotatedEle) { + final List annotations = new ArrayList<>(); + scan( + (index, annotation) -> annotations.add(annotation), annotatedEle, + annotation -> ObjectUtil.notEqual(annotation, annotatedEle) + ); + return annotations; + } + + /** + * 按广度优先扫描指定注解上的元注解,对扫描到的注解与层级索引进行操作 + * + * @param consumer 当前层级索引与操作 + * @param annotatedEle {@link AnnotatedElement},可以是Class、Method、Field、Constructor、ReflectPermission + * @param filter 过滤器 + */ + @SuppressWarnings("unchecked") + @Override + public void scan(BiConsumer consumer, AnnotatedElement annotatedEle, Predicate filter) { + filter = ObjectUtil.defaultIfNull(filter, a -> t -> true); + Set> accessed = new HashSet<>(); + final Deque>> deque = CollUtil.newLinkedList(CollUtil.newArrayList((Class) annotatedEle)); + int distance = 0; + do { + final List> annotationTypes = deque.removeFirst(); + for (final Class type : annotationTypes) { + final List metaAnnotations = Stream.of(type.getAnnotations()) + .filter(a -> !AnnotationUtil.isJdkMetaAnnotation(a.annotationType())) + .filter(filter) + .collect(Collectors.toList()); + for (final Annotation metaAnnotation : metaAnnotations) { + consumer.accept(distance, metaAnnotation); + } + accessed.add(type); + List> next = metaAnnotations.stream() + .map(Annotation::annotationType) + .filter(t -> !accessed.contains(t)) + .collect(Collectors.toList()); + if (CollUtil.isNotEmpty(next)) { + deque.addLast(next); + } + } + distance++; + } while (includeSupperMetaAnnotation && !deque.isEmpty()); + } + +} diff --git a/src/main/java/cn/hutool/core/annotation/scanner/MethodAnnotationScanner.java b/src/main/java/cn/hutool/core/annotation/scanner/MethodAnnotationScanner.java new file mode 100644 index 0000000..6e509f2 --- /dev/null +++ b/src/main/java/cn/hutool/core/annotation/scanner/MethodAnnotationScanner.java @@ -0,0 +1,133 @@ +package cn.hutool.core.annotation.scanner; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.ClassUtil; +import cn.hutool.core.util.StrUtil; + +import java.lang.annotation.Annotation; +import java.lang.reflect.AnnotatedElement; +import java.lang.reflect.Method; +import java.util.Set; +import java.util.function.Predicate; +import java.util.stream.Stream; + +/** + * 扫描{@link Method}上的注解 + * + * @author huangchengxing + */ +public class MethodAnnotationScanner extends AbstractTypeAnnotationScanner implements AnnotationScanner { + + /** + * 构造一个类注解扫描器,仅扫描该方法上直接声明的注解 + */ + public MethodAnnotationScanner() { + this(false); + } + + /** + * 构造一个类注解扫描器 + * + * @param scanSameSignatureMethod 是否扫描类层级结构中具有相同方法签名的方法 + */ + public MethodAnnotationScanner(boolean scanSameSignatureMethod) { + this(scanSameSignatureMethod, targetClass -> true, CollUtil.newLinkedHashSet()); + } + + /** + * 构造一个方法注解扫描器 + * + * @param scanSameSignatureMethod 是否扫描类层级结构中具有相同方法签名的方法 + * @param filter 过滤器 + * @param excludeTypes 不包含的类型 + */ + public MethodAnnotationScanner(boolean scanSameSignatureMethod, Predicate> filter, Set> excludeTypes) { + super(scanSameSignatureMethod, scanSameSignatureMethod, filter, excludeTypes); + } + + /** + * 构造一个方法注解扫描器 + * + * @param includeSuperClass 是否允许扫描父类中具有相同方法签名的方法 + * @param includeInterfaces 是否允许扫描父接口中具有相同方法签名的方法 + * @param filter 过滤器 + * @param excludeTypes 不包含的类型 + */ + public MethodAnnotationScanner(boolean includeSuperClass, boolean includeInterfaces, Predicate> filter, Set> excludeTypes) { + super(includeSuperClass, includeInterfaces, filter, excludeTypes); + } + + /** + * 判断是否支持扫描该注解元素,仅当注解元素是{@link Method}时返回{@code true} + * + * @param annotatedEle {@link AnnotatedElement},可以是Class、Method、Field、Constructor、ReflectPermission + * @return boolean 是否支持扫描该注解元素 + */ + @Override + public boolean support(AnnotatedElement annotatedEle) { + return annotatedEle instanceof Method; + } + + /** + * 获取声明该方法的类 + * + * @param annotatedElement 注解元素 + * @return 要递归的类型 + * @see Method#getDeclaringClass() + */ + @Override + protected Class getClassFormAnnotatedElement(AnnotatedElement annotatedElement) { + return ((Method)annotatedElement).getDeclaringClass(); + } + + /** + * 若父类/父接口中方法具有相同的方法签名,则返回该方法上的注解 + * + * @param source 原始方法 + * @param index 类的层级索引 + * @param targetClass 类 + * @return 最终所需的目标注解 + */ + @Override + protected Annotation[] getAnnotationsFromTargetClass(AnnotatedElement source, int index, Class targetClass) { + final Method sourceMethod = (Method) source; + return Stream.of(ClassUtil.getDeclaredMethods(targetClass)) + .filter(superMethod -> !superMethod.isBridge()) + .filter(superMethod -> hasSameSignature(sourceMethod, superMethod)) + .map(AnnotatedElement::getAnnotations) + .flatMap(Stream::of) + .toArray(Annotation[]::new); + } + + /** + * 设置是否扫描类层级结构中具有相同方法签名的方法 + * + * @param scanSuperMethodIfOverride 是否扫描类层级结构中具有相同方法签名的方法 + * @return 当前实例 + */ + public MethodAnnotationScanner setScanSameSignatureMethod(boolean scanSuperMethodIfOverride) { + setIncludeInterfaces(scanSuperMethodIfOverride); + setIncludeSuperClass(scanSuperMethodIfOverride); + return this; + } + + /** + * 该方法是否具备与扫描的方法相同的方法签名 + */ + private boolean hasSameSignature(Method sourceMethod, Method superMethod) { + if (!StrUtil.equals(sourceMethod.getName(), superMethod.getName())) { + return false; + } + final Class[] sourceParameterTypes = sourceMethod.getParameterTypes(); + final Class[] targetParameterTypes = superMethod.getParameterTypes(); + if (sourceParameterTypes.length != targetParameterTypes.length) { + return false; + } + if (!ArrayUtil.containsAll(sourceParameterTypes, targetParameterTypes)) { + return false; + } + return ClassUtil.isAssignable(superMethod.getReturnType(), sourceMethod.getReturnType()); + } + +} diff --git a/src/main/java/cn/hutool/core/annotation/scanner/TypeAnnotationScanner.java b/src/main/java/cn/hutool/core/annotation/scanner/TypeAnnotationScanner.java new file mode 100644 index 0000000..15b6fe5 --- /dev/null +++ b/src/main/java/cn/hutool/core/annotation/scanner/TypeAnnotationScanner.java @@ -0,0 +1,105 @@ +package cn.hutool.core.annotation.scanner; + +import cn.hutool.core.collection.CollUtil; + +import java.lang.annotation.Annotation; +import java.lang.reflect.AnnotatedElement; +import java.lang.reflect.Proxy; +import java.util.Set; +import java.util.function.Predicate; +import java.util.function.UnaryOperator; + +/** + * 扫描{@link Class}上的注解 + * + * @author huangchengxing + */ +public class TypeAnnotationScanner extends AbstractTypeAnnotationScanner implements AnnotationScanner { + + /** + * 构造一个类注解扫描器 + * + * @param includeSupperClass 是否允许扫描父类 + * @param includeInterfaces 是否允许扫描父接口 + * @param filter 过滤器 + * @param excludeTypes 不包含的类型 + */ + public TypeAnnotationScanner(boolean includeSupperClass, boolean includeInterfaces, Predicate> filter, Set> excludeTypes) { + super(includeSupperClass, includeInterfaces, filter, excludeTypes); + } + + /** + * 构建一个类注解扫描器,默认允许扫描指定元素的父类以及父接口 + */ + public TypeAnnotationScanner() { + this(true, true, t -> true, CollUtil.newLinkedHashSet()); + } + + /** + * 判断是否支持扫描该注解元素,仅当注解元素是{@link Class}接时返回{@code true} + * + * @param annotatedEle {@link AnnotatedElement},可以是Class、Method、Field、Constructor、ReflectPermission + * @return 是否支持扫描该注解元素 + */ + @Override + public boolean support(AnnotatedElement annotatedEle) { + return annotatedEle instanceof Class; + } + + /** + * 将注解元素转为{@link Class} + * + * @param annotatedEle {@link AnnotatedElement},可以是Class、Method、Field、Constructor、ReflectPermission + * @return 要递归的类型 + */ + @Override + protected Class getClassFormAnnotatedElement(AnnotatedElement annotatedEle) { + return (Class)annotatedEle; + } + + /** + * 获取{@link Class#getAnnotations()} + * + * @param source 最初的注解元素 + * @param index 类的层级索引 + * @param targetClass 类 + * @return 类上直接声明的注解 + */ + @Override + protected Annotation[] getAnnotationsFromTargetClass(AnnotatedElement source, int index, Class targetClass) { + return targetClass.getAnnotations(); + } + + /** + * 是否允许扫描父类 + * + * @param includeSuperClass 是否允许扫描父类 + * @return 当前实例 + */ + @Override + public TypeAnnotationScanner setIncludeSuperClass(boolean includeSuperClass) { + return super.setIncludeSuperClass(includeSuperClass); + } + + /** + * 是否允许扫描父接口 + * + * @param includeInterfaces 是否允许扫描父类 + * @return 当前实例 + */ + @Override + public TypeAnnotationScanner setIncludeInterfaces(boolean includeInterfaces) { + return super.setIncludeInterfaces(includeInterfaces); + } + + /** + * 若类型为jdk代理类,则尝试转换为原始被代理类 + */ + public static class JdkProxyClassConverter implements UnaryOperator> { + @Override + public Class apply(Class sourceClass) { + return Proxy.isProxyClass(sourceClass) ? apply(sourceClass.getSuperclass()) : sourceClass; + } + } + +} diff --git a/src/main/java/cn/hutool/core/annotation/scanner/package-info.java b/src/main/java/cn/hutool/core/annotation/scanner/package-info.java new file mode 100644 index 0000000..3d42d61 --- /dev/null +++ b/src/main/java/cn/hutool/core/annotation/scanner/package-info.java @@ -0,0 +1,7 @@ +/** + * 注解包扫描封装 + * + * @author looly + * + */ +package cn.hutool.core.annotation.scanner; diff --git a/src/main/java/cn/hutool/core/bean/BeanDesc.java b/src/main/java/cn/hutool/core/bean/BeanDesc.java new file mode 100644 index 0000000..ed20f2e --- /dev/null +++ b/src/main/java/cn/hutool/core/bean/BeanDesc.java @@ -0,0 +1,324 @@ +package cn.hutool.core.bean; + +import cn.hutool.core.lang.Assert; +import cn.hutool.core.map.CaseInsensitiveMap; +import cn.hutool.core.util.BooleanUtil; +import cn.hutool.core.util.ModifierUtil; +import cn.hutool.core.util.ReflectUtil; +import cn.hutool.core.util.StrUtil; + +import java.io.Serializable; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * Bean信息描述做为BeanInfo替代方案,此对象持有JavaBean中的setters和getters等相关信息描述
+ * 查找Getter和Setter方法时会: + * + *
+ * 1. 忽略字段和方法名的大小写
+ * 2. Getter查找getXXX、isXXX、getIsXXX
+ * 3. Setter查找setXXX、setIsXXX
+ * 4. Setter忽略参数值与字段值不匹配的情况,因此有多个参数类型的重载时,会调用首次匹配的
+ * 
+ * + * @author looly + * @since 3.1.2 + */ +public class BeanDesc implements Serializable { + private static final long serialVersionUID = 1L; + + /** + * Bean类 + */ + private final Class beanClass; + /** + * 属性Map + */ + private final Map propMap = new LinkedHashMap<>(); + + /** + * 构造 + * + * @param beanClass Bean类 + */ + public BeanDesc(Class beanClass) { + Assert.notNull(beanClass); + this.beanClass = beanClass; + init(); + } + + /** + * 获取Bean的全类名 + * + * @return Bean的类名 + */ + public String getName() { + return this.beanClass.getName(); + } + + /** + * 获取Bean的简单类名 + * + * @return Bean的类名 + */ + public String getSimpleName() { + return this.beanClass.getSimpleName(); + } + + /** + * 获取字段名-字段属性Map + * + * @param ignoreCase 是否忽略大小写,true为忽略,false不忽略 + * @return 字段名-字段属性Map + */ + public Map getPropMap(boolean ignoreCase) { + return ignoreCase ? new CaseInsensitiveMap<>(1, this.propMap) : this.propMap; + } + + /** + * 获取字段属性列表 + * + * @return {@link PropDesc} 列表 + */ + public Collection getProps() { + return this.propMap.values(); + } + + /** + * 获取属性,如果不存在返回null + * + * @param fieldName 字段名 + * @return {@link PropDesc} + */ + public PropDesc getProp(String fieldName) { + return this.propMap.get(fieldName); + } + + /** + * 获得字段名对应的字段对象,如果不存在返回null + * + * @param fieldName 字段名 + * @return 字段值 + */ + public Field getField(String fieldName) { + final PropDesc desc = this.propMap.get(fieldName); + return null == desc ? null : desc.getField(); + } + + /** + * 获取Getter方法,如果不存在返回null + * + * @param fieldName 字段名 + * @return Getter方法 + */ + public Method getGetter(String fieldName) { + final PropDesc desc = this.propMap.get(fieldName); + return null == desc ? null : desc.getGetter(); + } + + /** + * 获取Setter方法,如果不存在返回null + * + * @param fieldName 字段名 + * @return Setter方法 + */ + public Method getSetter(String fieldName) { + final PropDesc desc = this.propMap.get(fieldName); + return null == desc ? null : desc.getSetter(); + } + + // ------------------------------------------------------------------------------------------------------ Private method start + + /** + * 初始化
+ * 只有与属性关联的相关Getter和Setter方法才会被读取,无关的getXXX和setXXX都被忽略 + * + * @return this + */ + private BeanDesc init() { + final Method[] gettersAndSetters = ReflectUtil.getMethods(this.beanClass, ReflectUtil::isGetterOrSetterIgnoreCase); + PropDesc prop; + for (Field field : ReflectUtil.getFields(this.beanClass)) { + // 排除静态属性和对象子类 + if (!ModifierUtil.isStatic(field) && !ReflectUtil.isOuterClassField(field)) { + prop = createProp(field, gettersAndSetters); + // 只有不存在时才放入,防止父类属性覆盖子类属性 + this.propMap.putIfAbsent(prop.getFieldName(), prop); + } + } + return this; + } + + /** + * 根据字段创建属性描述
+ * 查找Getter和Setter方法时会: + * + *
+	 * 1. 忽略字段和方法名的大小写
+	 * 2. Getter查找getXXX、isXXX、getIsXXX
+	 * 3. Setter查找setXXX、setIsXXX
+	 * 4. Setter忽略参数值与字段值不匹配的情况,因此有多个参数类型的重载时,会调用首次匹配的
+	 * 
+ * + * @param field 字段 + * @param methods 类中所有的方法 + * @return {@link PropDesc} + * @since 4.0.2 + */ + private PropDesc createProp(Field field, Method[] methods) { + final PropDesc prop = findProp(field, methods, false); + // 忽略大小写重新匹配一次 + if (null == prop.getter || null == prop.setter) { + final PropDesc propIgnoreCase = findProp(field, methods, true); + if (null == prop.getter) { + prop.getter = propIgnoreCase.getter; + } + if (null == prop.setter) { + prop.setter = propIgnoreCase.setter; + } + } + + return prop; + } + + /** + * 查找字段对应的Getter和Setter方法 + * + * @param field 字段 + * @param gettersOrSetters 类中所有的Getter或Setter方法 + * @param ignoreCase 是否忽略大小写匹配 + * @return PropDesc + */ + private PropDesc findProp(Field field, Method[] gettersOrSetters, boolean ignoreCase) { + final String fieldName = field.getName(); + final Class fieldType = field.getType(); + final boolean isBooleanField = BooleanUtil.isBoolean(fieldType); + + Method getter = null; + Method setter = null; + String methodName; + for (Method method : gettersOrSetters) { + methodName = method.getName(); + if (method.getParameterCount() == 0) { + // 无参数,可能为Getter方法 + if (isMatchGetter(methodName, fieldName, isBooleanField, ignoreCase)) { + // 方法名与字段名匹配,则为Getter方法 + getter = method; + } + } else if (isMatchSetter(methodName, fieldName, isBooleanField, ignoreCase)) { + // setter方法的参数类型和字段类型必须一致,或参数类型是字段类型的子类 + if(fieldType.isAssignableFrom(method.getParameterTypes()[0])){ + setter = method; + } + } + if (null != getter && null != setter) { + // 如果Getter和Setter方法都找到了,不再继续寻找 + break; + } + } + + return new PropDesc(field, getter, setter); + } + + /** + * 方法是否为Getter方法
+ * 匹配规则如下(忽略大小写): + * + *
+	 * 字段名    -》 方法名
+	 * isName  -》 isName
+	 * isName  -》 isIsName
+	 * isName  -》 getIsName
+	 * name     -》 isName
+	 * name     -》 getName
+	 * 
+ * + * @param methodName 方法名 + * @param fieldName 字段名 + * @param isBooleanField 是否为Boolean类型字段 + * @param ignoreCase 匹配是否忽略大小写 + * @return 是否匹配 + */ + private boolean isMatchGetter(String methodName, String fieldName, boolean isBooleanField, boolean ignoreCase) { + final String handledFieldName; + if (ignoreCase) { + // 全部转为小写,忽略大小写比较 + methodName = methodName.toLowerCase(); + handledFieldName = fieldName.toLowerCase(); + fieldName = handledFieldName; + } else { + handledFieldName = StrUtil.upperFirst(fieldName); + } + + // 针对Boolean类型特殊检查 + if (isBooleanField) { + if (fieldName.startsWith("is")) { + // 字段已经是is开头 + if (methodName.equals(fieldName) // isName -》 isName + || ("get" + handledFieldName).equals(methodName)// isName -》 getIsName + || ("is" + handledFieldName).equals(methodName)// isName -》 isIsName + ) { + return true; + } + } else if (("is" + handledFieldName).equals(methodName)) { + // 字段非is开头, name -》 isName + return true; + } + } + + // 包括boolean的任何类型只有一种匹配情况:name -》 getName + return ("get" + handledFieldName).equals(methodName); + } + + /** + * 方法是否为Setter方法
+ * 匹配规则如下(忽略大小写): + * + *
+	 * 字段名    -》 方法名
+	 * isName  -》 setName
+	 * isName  -》 setIsName
+	 * name     -》 setName
+	 * 
+ * + * @param methodName 方法名 + * @param fieldName 字段名 + * @param isBooleanField 是否为Boolean类型字段 + * @param ignoreCase 匹配是否忽略大小写 + * @return 是否匹配 + */ + private boolean isMatchSetter(String methodName, String fieldName, boolean isBooleanField, boolean ignoreCase) { + final String handledFieldName; + if (ignoreCase) { + // 全部转为小写,忽略大小写比较 + methodName = methodName.toLowerCase(); + handledFieldName = fieldName.toLowerCase(); + fieldName = handledFieldName; + } else { + handledFieldName = StrUtil.upperFirst(fieldName); + } + + // 非标准Setter方法跳过 + if (!methodName.startsWith("set")) { + return false; + } + + // 针对Boolean类型特殊检查 + if (isBooleanField && fieldName.startsWith("is")) { + // 字段是is开头 + if (("set" + StrUtil.removePrefix(fieldName, "is")).equals(methodName)// isName -》 setName + || ("set" + handledFieldName).equals(methodName)// isName -》 setIsName + ) { + return true; + } + } + + // 包括boolean的任何类型只有一种匹配情况:name -》 setName + return ("set" + handledFieldName).equals(methodName); + } + // ------------------------------------------------------------------------------------------------------ Private method end +} diff --git a/src/main/java/cn/hutool/core/bean/BeanDescCache.java b/src/main/java/cn/hutool/core/bean/BeanDescCache.java new file mode 100644 index 0000000..fa29e1d --- /dev/null +++ b/src/main/java/cn/hutool/core/bean/BeanDescCache.java @@ -0,0 +1,37 @@ +package cn.hutool.core.bean; + +import cn.hutool.core.lang.func.Func0; +import cn.hutool.core.map.WeakConcurrentMap; + +/** + * Bean属性缓存
+ * 缓存用于防止多次反射造成的性能问题 + * + * @author Looly + */ +public enum BeanDescCache { + INSTANCE; + + private final WeakConcurrentMap, BeanDesc> bdCache = new WeakConcurrentMap<>(); + + /** + * 获得属性名和{@link BeanDesc}Map映射 + * + * @param beanClass Bean的类 + * @param supplier 对象不存在时创建对象的函数 + * @return 属性名和{@link BeanDesc}映射 + * @since 5.4.2 + */ + public BeanDesc getBeanDesc(Class beanClass, Func0 supplier) { + return bdCache.computeIfAbsent(beanClass, (key)->supplier.callWithRuntimeException()); + } + + /** + * 清空全局的Bean属性缓存 + * + * @since 5.7.21 + */ + public void clear() { + this.bdCache.clear(); + } +} diff --git a/src/main/java/cn/hutool/core/bean/BeanException.java b/src/main/java/cn/hutool/core/bean/BeanException.java new file mode 100644 index 0000000..227e280 --- /dev/null +++ b/src/main/java/cn/hutool/core/bean/BeanException.java @@ -0,0 +1,32 @@ +package cn.hutool.core.bean; + +import cn.hutool.core.exceptions.ExceptionUtil; +import cn.hutool.core.util.StrUtil; + +/** + * Bean异常 + * @author xiaoleilu + */ +public class BeanException extends RuntimeException{ + private static final long serialVersionUID = -8096998667745023423L; + + public BeanException(Throwable e) { + super(ExceptionUtil.getMessage(e), e); + } + + public BeanException(String message) { + super(message); + } + + public BeanException(String messageTemplate, Object... params) { + super(StrUtil.format(messageTemplate, params)); + } + + public BeanException(String message, Throwable throwable) { + super(message, throwable); + } + + public BeanException(Throwable throwable, String messageTemplate, Object... params) { + super(StrUtil.format(messageTemplate, params), throwable); + } +} diff --git a/src/main/java/cn/hutool/core/bean/BeanPath.java b/src/main/java/cn/hutool/core/bean/BeanPath.java new file mode 100644 index 0000000..3a7d153 --- /dev/null +++ b/src/main/java/cn/hutool/core/bean/BeanPath.java @@ -0,0 +1,324 @@ +package cn.hutool.core.bean; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.collection.ListUtil; +import cn.hutool.core.convert.Convert; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.CharUtil; +import cn.hutool.core.util.NumberUtil; +import cn.hutool.core.util.StrUtil; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Bean路径表达式,用于获取多层嵌套Bean中的字段值或Bean对象
+ * 根据给定的表达式,查找Bean中对应的属性值对象。 表达式分为两种: + *
    + *
  1. .表达式,可以获取Bean对象中的属性(字段)值或者Map中key对应的值
  2. + *
  3. []表达式,可以获取集合等对象中对应index的值
  4. + *
+ *

+ * 表达式栗子: + * + *

+ * persion
+ * persion.name
+ * persons[3]
+ * person.friends[5].name
+ * ['person']['friends'][5]['name']
+ * 
+ * + * @author Looly + * @since 4.0.6 + */ +public class BeanPath implements Serializable { + private static final long serialVersionUID = 1L; + + /** + * 表达式边界符号数组 + */ + private static final char[] EXP_CHARS = {CharUtil.DOT, CharUtil.BRACKET_START, CharUtil.BRACKET_END}; + + private boolean isStartWith = false; + protected List patternParts; + + /** + * 解析Bean路径表达式为Bean模式
+ * Bean表达式,用于获取多层嵌套Bean中的字段值或Bean对象
+ * 根据给定的表达式,查找Bean中对应的属性值对象。 表达式分为两种: + *
    + *
  1. .表达式,可以获取Bean对象中的属性(字段)值或者Map中key对应的值
  2. + *
  3. []表达式,可以获取集合等对象中对应index的值
  4. + *
+ *

+ * 表达式栗子: + * + *

+	 * persion
+	 * persion.name
+	 * persons[3]
+	 * person.friends[5].name
+	 * ['person']['friends'][5]['name']
+	 * 
+ * + * @param expression 表达式 + * @return BeanPath + */ + public static BeanPath create(final String expression) { + return new BeanPath(expression); + } + + /** + * 构造 + * + * @param expression 表达式 + */ + public BeanPath(final String expression) { + init(expression); + } + + /** + * 获取表达式解析后的分段列表 + * + * @return 表达式分段列表 + */ + public List getPatternParts() { + return this.patternParts; + } + + /** + * 获取Bean中对应表达式的值 + * + * @param bean Bean对象或Map或List等 + * @return 值,如果对应值不存在,则返回null + */ + public Object get(final Object bean) { + return get(this.patternParts, bean, false); + } + + /** + * 设置表达式指定位置(或filed对应)的值
+ * 若表达式指向一个List则设置其坐标对应位置的值,若指向Map则put对应key的值,Bean则设置字段的值
+ * 注意: + * + *
+	 * 1. 如果为List,如果下标不大于List长度,则替换原有值,否则追加值
+	 * 2. 如果为数组,如果下标不大于数组长度,则替换原有值,否则追加值
+	 * 
+ * + * @param bean Bean、Map或List + * @param value 值 + */ + public void set(final Object bean, final Object value) { + set(bean, this.patternParts, lastIsNumber(this.patternParts), value); + } + + @Override + public String toString() { + return this.patternParts.toString(); + } + + //region Private Methods + + /** + * 设置表达式指定位置(或filed对应)的值
+ * 若表达式指向一个List则设置其坐标对应位置的值,若指向Map则put对应key的值,Bean则设置字段的值
+ * 注意: + * + *
+	 * 1. 如果为List,如果下标不大于List长度,则替换原有值,否则追加值
+	 * 2. 如果为数组,如果下标不大于数组长度,则替换原有值,否则追加值
+	 * 
+ * + * @param bean Bean、Map或List + * @param patternParts 表达式块列表 + * @param value 值 + * @return 值 + */ + private void set(Object bean, List patternParts, boolean nextNumberPart, Object value) { + Object subBean = this.get(patternParts, bean, true); + if (null == subBean) { + final List parentParts = getParentParts(patternParts); + this.set(bean, parentParts, lastIsNumber(parentParts), nextNumberPart ? new ArrayList<>() : new HashMap<>()); + //set中有可能做过转换,因此此处重新获取bean + subBean = this.get(patternParts, bean, true); + } + BeanUtil.setFieldValue(subBean, patternParts.get(patternParts.size() - 1), value); + } + + /** + * 判断path列表中末尾的标记是否为数字 + * + * @param patternParts path列表 + * @return 是否为数字 + */ + private static boolean lastIsNumber(List patternParts) { + return NumberUtil.isInteger(patternParts.get(patternParts.size() - 1)); + } + + /** + * 获取父级路径列表 + * + * @param patternParts 路径列表 + * @return 父级路径列表 + */ + private static List getParentParts(List patternParts) { + return patternParts.subList(0, patternParts.size() - 1); + } + + /** + * 获取Bean中对应表达式的值 + * + * @param patternParts 表达式分段列表 + * @param bean Bean对象或Map或List等 + * @param ignoreLast 是否忽略最后一个值,忽略最后一个值则用于set,否则用于read + * @return 值,如果对应值不存在,则返回null + */ + private Object get(final List patternParts, final Object bean, final boolean ignoreLast) { + int length = patternParts.size(); + if (ignoreLast) { + length--; + } + Object subBean = bean; + boolean isFirst = true; + String patternPart; + for (int i = 0; i < length; i++) { + patternPart = patternParts.get(i); + subBean = getFieldValue(subBean, patternPart); + if (null == subBean) { + // 支持表达式的第一个对象为Bean本身(若用户定义表达式$开头,则不做此操作) + if (isFirst && !this.isStartWith && BeanUtil.isMatchName(bean, patternPart, true)) { + subBean = bean; + isFirst = false; + } else { + return null; + } + } + } + return subBean; + } + + @SuppressWarnings("unchecked") + private static Object getFieldValue(final Object bean, final String expression) { + if (StrUtil.isBlank(expression)) { + return null; + } + + if (StrUtil.contains(expression, ':')) { + // [start:end:step] 模式 + final List parts = StrUtil.splitTrim(expression, ':'); + final int start = Integer.parseInt(parts.get(0)); + final int end = Integer.parseInt(parts.get(1)); + int step = 1; + if (3 == parts.size()) { + step = Integer.parseInt(parts.get(2)); + } + if (bean instanceof Collection) { + return CollUtil.sub((Collection) bean, start, end, step); + } else if (ArrayUtil.isArray(bean)) { + return ArrayUtil.sub(bean, start, end, step); + } + } else if (StrUtil.contains(expression, ',')) { + // [num0,num1,num2...]模式或者['key0','key1']模式 + final List keys = StrUtil.splitTrim(expression, ','); + if (bean instanceof Collection) { + return CollUtil.getAny((Collection) bean, Convert.convert(int[].class, keys)); + } else if (ArrayUtil.isArray(bean)) { + return ArrayUtil.getAny(bean, Convert.convert(int[].class, keys)); + } else { + final String[] unWrappedKeys = new String[keys.size()]; + for (int i = 0; i < unWrappedKeys.length; i++) { + unWrappedKeys[i] = StrUtil.unWrap(keys.get(i), '\''); + } + if (bean instanceof Map) { + // 只支持String为key的Map + return MapUtil.getAny((Map) bean, unWrappedKeys); + } else { + final Map map = BeanUtil.beanToMap(bean); + return MapUtil.getAny(map, unWrappedKeys); + } + } + } else { + // 数字或普通字符串 + return BeanUtil.getFieldValue(bean, expression); + } + + return null; + } + + /** + * 初始化 + * + * @param expression 表达式 + */ + private void init(final String expression) { + final List localPatternParts = new ArrayList<>(); + final int length = expression.length(); + + final StringBuilder builder = new StringBuilder(); + char c; + boolean isNumStart = false;// 下标标识符开始 + boolean isInWrap = false; //标识是否在引号内 + for (int i = 0; i < length; i++) { + c = expression.charAt(i); + if (0 == i && '$' == c) { + // 忽略开头的$符,表示当前对象 + isStartWith = true; + continue; + } + + if ('\'' == c) { + // 结束 + isInWrap = (!isInWrap); + continue; + } + + if (!isInWrap && ArrayUtil.contains(EXP_CHARS, c)) { + // 处理边界符号 + if (CharUtil.BRACKET_END == c) { + // 中括号(数字下标)结束 + if (!isNumStart) { + throw new IllegalArgumentException(StrUtil.format("Bad expression '{}':{}, we find ']' but no '[' !", expression, i)); + } + isNumStart = false; + // 中括号结束加入下标 + } else { + if (isNumStart) { + // 非结束中括号情况下发现起始中括号报错(中括号未关闭) + throw new IllegalArgumentException(StrUtil.format("Bad expression '{}':{}, we find '[' but no ']' !", expression, i)); + } else if (CharUtil.BRACKET_START == c) { + // 数字下标开始 + isNumStart = true; + } + // 每一个边界符之前的表达式是一个完整的KEY,开始处理KEY + } + if (builder.length() > 0) { + localPatternParts.add(builder.toString()); + } + builder.setLength(0); + } else { + // 非边界符号,追加字符 + builder.append(c); + } + } + + // 末尾边界符检查 + if (isNumStart) { + throw new IllegalArgumentException(StrUtil.format("Bad expression '{}':{}, we find '[' but no ']' !", expression, length - 1)); + } else { + if (builder.length() > 0) { + localPatternParts.add(builder.toString()); + } + } + + // 不可变List + this.patternParts = ListUtil.unmodifiable(localPatternParts); + } + //endregion +} diff --git a/src/main/java/cn/hutool/core/bean/BeanUtil.java b/src/main/java/cn/hutool/core/bean/BeanUtil.java new file mode 100644 index 0000000..254f871 --- /dev/null +++ b/src/main/java/cn/hutool/core/bean/BeanUtil.java @@ -0,0 +1,917 @@ +package cn.hutool.core.bean; + +import cn.hutool.core.bean.copier.BeanCopier; +import cn.hutool.core.bean.copier.CopyOptions; +import cn.hutool.core.bean.copier.ValueProvider; +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.collection.ListUtil; +import cn.hutool.core.convert.Convert; +import cn.hutool.core.lang.Editor; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.ClassUtil; +import cn.hutool.core.util.ModifierUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.ReflectUtil; +import cn.hutool.core.util.StrUtil; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Consumer; +import java.util.function.Supplier; +import java.util.stream.Collectors; + +/** + * Bean工具类 + * + *

+ * 把一个拥有对属性进行set和get方法的类,我们就可以称之为JavaBean。 + *

+ * + * @author Looly + * @since 3.1.2 + */ +public class BeanUtil { + + /** + * 判断是否为可读的Bean对象,判定方法是: + * + *
+	 *     1、是否存在只有无参数的getXXX方法或者isXXX方法
+	 *     2、是否存在public类型的字段
+	 * 
+ * + * @param clazz 待测试类 + * @return 是否为可读的Bean对象 + * @see #hasGetter(Class) + * @see #hasPublicField(Class) + */ + public static boolean isReadableBean(Class clazz) { + return hasGetter(clazz) || hasPublicField(clazz); + } + + /** + * 判断是否为Bean对象,判定方法是: + * + *
+	 *     1、是否存在只有一个参数的setXXX方法
+	 *     2、是否存在public类型的字段
+	 * 
+ * + * @param clazz 待测试类 + * @return 是否为Bean对象 + * @see #hasSetter(Class) + * @see #hasPublicField(Class) + */ + public static boolean isBean(Class clazz) { + return hasSetter(clazz) || hasPublicField(clazz); + } + + /** + * 判断是否有Setter方法
+ * 判定方法是否存在只有一个参数的setXXX方法 + * + * @param clazz 待测试类 + * @return 是否为Bean对象 + * @since 4.2.2 + */ + public static boolean hasSetter(Class clazz) { + if (ClassUtil.isNormalClass(clazz)) { + for (Method method : clazz.getMethods()) { + if (method.getParameterCount() == 1 && method.getName().startsWith("set")) { + // 检测包含标准的setXXX方法即视为标准的JavaBean + return true; + } + } + } + return false; + } + + /** + * 判断是否为Bean对象
+ * 判定方法是否存在只有无参数的getXXX方法或者isXXX方法 + * + * @param clazz 待测试类 + * @return 是否为Bean对象 + * @since 4.2.2 + */ + public static boolean hasGetter(Class clazz) { + if (ClassUtil.isNormalClass(clazz)) { + for (Method method : clazz.getMethods()) { + if (method.getParameterCount() == 0) { + if (method.getName().startsWith("get") || method.getName().startsWith("is")) { + return true; + } + } + } + } + return false; + } + + /** + * 指定类中是否有public类型字段(static字段除外) + * + * @param clazz 待测试类 + * @return 是否有public类型字段 + * @since 5.1.0 + */ + public static boolean hasPublicField(Class clazz) { + if (ClassUtil.isNormalClass(clazz)) { + for (Field field : clazz.getFields()) { + if (ModifierUtil.isPublic(field) && !ModifierUtil.isStatic(field)) { + //非static的public字段 + return true; + } + } + } + return false; + } + + /** + * 创建动态Bean + * + * @param bean 普通Bean或Map + * @return {@link DynaBean} + * @since 3.0.7 + */ + public static DynaBean createDynaBean(Object bean) { + return new DynaBean(bean); + } + + + /** + * 获取{@link BeanDesc} Bean描述信息 + * + * @param clazz Bean类 + * @return {@link BeanDesc} + * @since 3.1.2 + */ + public static BeanDesc getBeanDesc(Class clazz) { + return BeanDescCache.INSTANCE.getBeanDesc(clazz, () -> new BeanDesc(clazz)); + } + + /** + * 遍历Bean的属性 + * + * @param clazz Bean类 + * @param action 每个元素的处理类 + * @since 5.4.2 + */ + public static void descForEach(Class clazz, Consumer action) { + getBeanDesc(clazz).getProps().forEach(action); + } + + // --------------------------------------------------------------------------------------------------------- PropertyDescriptor + + + /** + * 获得字段值,通过反射直接获得字段值,并不调用getXXX方法
+ * 对象同样支持Map类型,fieldNameOrIndex即为key + * + *
    + *
  • Map: fieldNameOrIndex需为key,获取对应value
  • + *
  • Collection: fieldNameOrIndex当为数字,返回index对应值,非数字遍历集合返回子bean对应name值
  • + *
  • Array: fieldNameOrIndex当为数字,返回index对应值,非数字遍历数组返回子bean对应name值
  • + *
+ * + * @param bean Bean对象 + * @param fieldNameOrIndex 字段名或序号,序号支持负数 + * @return 字段值 + */ + public static Object getFieldValue(Object bean, String fieldNameOrIndex) { + if (null == bean || null == fieldNameOrIndex) { + return null; + } + + if (bean instanceof Map) { + return ((Map) bean).get(fieldNameOrIndex); + } else if (bean instanceof Collection) { + try { + return CollUtil.get((Collection) bean, Integer.parseInt(fieldNameOrIndex)); + } catch (NumberFormatException e) { + // 非数字,see pr#254@Gitee + return CollUtil.map((Collection) bean, (beanEle) -> getFieldValue(beanEle, fieldNameOrIndex), false); + } + } else if (ArrayUtil.isArray(bean)) { + try { + return ArrayUtil.get(bean, Integer.parseInt(fieldNameOrIndex)); + } catch (NumberFormatException e) { + // 非数字,see pr#254@Gitee + return ArrayUtil.map(bean, Object.class, (beanEle) -> getFieldValue(beanEle, fieldNameOrIndex)); + } + } else {// 普通Bean对象 + return ReflectUtil.getFieldValue(bean, fieldNameOrIndex); + } + } + + /** + * 设置字段值,通过反射设置字段值,并不调用setXXX方法
+ * 对象同样支持Map类型,fieldNameOrIndex即为key + * + * @param bean Bean + * @param fieldNameOrIndex 字段名或序号,序号支持负数 + * @param value 值 + */ + @SuppressWarnings({"unchecked", "rawtypes"}) + public static void setFieldValue(Object bean, String fieldNameOrIndex, Object value) { + if (bean instanceof Map) { + ((Map) bean).put(fieldNameOrIndex, value); + } else if (bean instanceof List) { + ListUtil.setOrPadding((List) bean, Convert.toInt(fieldNameOrIndex), value); + } else if (ArrayUtil.isArray(bean)) { + ArrayUtil.setOrAppend(bean, Convert.toInt(fieldNameOrIndex), value); + } else { + // 普通Bean对象 + ReflectUtil.setFieldValue(bean, fieldNameOrIndex, value); + } + } + + /** + * 解析Bean中的属性值 + * + * @param 属性值类型 + * @param bean Bean对象,支持Map、List、Collection、Array + * @param expression 表达式,例如:person.friend[5].name + * @return Bean属性值,bean为{@code null}或者express为空,返回{@code null} + * @see BeanPath#get(Object) + * @since 3.0.7 + */ + @SuppressWarnings("unchecked") + public static T getProperty(Object bean, String expression) { + if (null == bean || StrUtil.isBlank(expression)) { + return null; + } + return (T) BeanPath.create(expression).get(bean); + } + + /** + * 解析Bean中的属性值 + * + * @param bean Bean对象,支持Map、List、Collection、Array + * @param expression 表达式,例如:person.friend[5].name + * @param value 属性值 + * @see BeanPath#get(Object) + * @since 4.0.6 + */ + public static void setProperty(Object bean, String expression, Object value) { + BeanPath.create(expression).set(bean, value); + } + + // --------------------------------------------------------------------------------------------- mapToBean + + /** + * Map转换为Bean对象 + * + * @param Bean类型 + * @param map {@link Map} + * @param beanClass Bean Class + * @param isIgnoreError 是否忽略注入错误 + * @return Bean + * @deprecated 请使用 {@link #toBean(Object, Class)} 或 {@link #toBeanIgnoreError(Object, Class)} + */ + @Deprecated + public static T mapToBean(Map map, Class beanClass, boolean isIgnoreError) { + return fillBeanWithMap(map, ReflectUtil.newInstanceIfPossible(beanClass), isIgnoreError); + } + + /** + * Map转换为Bean对象
+ * 忽略大小写 + * + * @param Bean类型 + * @param map Map + * @param beanClass Bean Class + * @param isIgnoreError 是否忽略注入错误 + * @return Bean + * @deprecated 请使用 {@link #toBeanIgnoreCase(Object, Class, boolean)} + */ + @Deprecated + public static T mapToBeanIgnoreCase(Map map, Class beanClass, boolean isIgnoreError) { + return fillBeanWithMapIgnoreCase(map, ReflectUtil.newInstanceIfPossible(beanClass), isIgnoreError); + } + + /** + * Map转换为Bean对象 + * + * @param Bean类型 + * @param map {@link Map} + * @param beanClass Bean Class + * @param copyOptions 转Bean选项 + * @return Bean + * @deprecated 请使用 {@link #toBean(Object, Class, CopyOptions)} + */ + @Deprecated + public static T mapToBean(Map map, Class beanClass, CopyOptions copyOptions) { + return fillBeanWithMap(map, ReflectUtil.newInstanceIfPossible(beanClass), copyOptions); + } + + /** + * Map转换为Bean对象 + * + * @param Bean类型 + * @param map {@link Map} + * @param beanClass Bean Class + * @param isToCamelCase 是否将Map中的下划线风格key转换为驼峰风格 + * @param copyOptions 转Bean选项 + * @return Bean + */ + public static T mapToBean(Map map, Class beanClass, boolean isToCamelCase, CopyOptions copyOptions) { + return fillBeanWithMap(map, ReflectUtil.newInstanceIfPossible(beanClass), isToCamelCase, copyOptions); + } + + // --------------------------------------------------------------------------------------------- fillBeanWithMap + + /** + * 使用Map填充Bean对象 + * + * @param Bean类型 + * @param map Map + * @param bean Bean + * @param isIgnoreError 是否忽略注入错误 + * @return Bean + */ + public static T fillBeanWithMap(Map map, T bean, boolean isIgnoreError) { + return fillBeanWithMap(map, bean, false, isIgnoreError); + } + + /** + * 使用Map填充Bean对象,可配置将下划线转换为驼峰 + * + * @param Bean类型 + * @param map Map + * @param bean Bean + * @param isToCamelCase 是否将下划线模式转换为驼峰模式 + * @param isIgnoreError 是否忽略注入错误 + * @return Bean + */ + public static T fillBeanWithMap(Map map, T bean, boolean isToCamelCase, boolean isIgnoreError) { + return fillBeanWithMap(map, bean, isToCamelCase, CopyOptions.create().setIgnoreError(isIgnoreError)); + } + + /** + * 使用Map填充Bean对象,忽略大小写 + * + * @param Bean类型 + * @param map Map + * @param bean Bean + * @param isIgnoreError 是否忽略注入错误 + * @return Bean + */ + public static T fillBeanWithMapIgnoreCase(Map map, T bean, boolean isIgnoreError) { + return fillBeanWithMap(map, bean, CopyOptions.create().setIgnoreCase(true).setIgnoreError(isIgnoreError)); + } + + /** + * 使用Map填充Bean对象 + * + * @param Bean类型 + * @param map Map + * @param bean Bean + * @param copyOptions 属性复制选项 {@link CopyOptions} + * @return Bean + */ + public static T fillBeanWithMap(Map map, T bean, CopyOptions copyOptions) { + return fillBeanWithMap(map, bean, false, copyOptions); + } + + /** + * 使用Map填充Bean对象 + * + * @param Bean类型 + * @param map Map + * @param bean Bean + * @param isToCamelCase 是否将Map中的下划线风格key转换为驼峰风格 + * @param copyOptions 属性复制选项 {@link CopyOptions} + * @return Bean + * @since 3.3.1 + */ + public static T fillBeanWithMap(Map map, T bean, boolean isToCamelCase, CopyOptions copyOptions) { + if (MapUtil.isEmpty(map)) { + return bean; + } + if (isToCamelCase) { + map = MapUtil.toCamelCaseMap(map); + } + copyProperties(map, bean, copyOptions); + return bean; + } + + // --------------------------------------------------------------------------------------------- fillBean + + /** + * 对象或Map转Bean + * + * @param 转换的Bean类型 + * @param source Bean对象或Map + * @param clazz 目标的Bean类型 + * @return Bean对象 + * @since 4.1.20 + */ + public static T toBean(Object source, Class clazz) { + return toBean(source, clazz, null); + } + + /** + * 对象或Map转Bean,忽略字段转换时发生的异常 + * + * @param 转换的Bean类型 + * @param source Bean对象或Map + * @param clazz 目标的Bean类型 + * @return Bean对象 + * @since 5.4.0 + */ + public static T toBeanIgnoreError(Object source, Class clazz) { + return toBean(source, clazz, CopyOptions.create().setIgnoreError(true)); + } + + /** + * 对象或Map转Bean,忽略字段转换时发生的异常 + * + * @param 转换的Bean类型 + * @param source Bean对象或Map + * @param clazz 目标的Bean类型 + * @param ignoreError 是否忽略注入错误 + * @return Bean对象 + * @since 5.4.0 + */ + public static T toBeanIgnoreCase(Object source, Class clazz, boolean ignoreError) { + return toBean(source, clazz, + CopyOptions.create() + .setIgnoreCase(true) + .setIgnoreError(ignoreError)); + } + + /** + * 对象或Map转Bean + * + * @param 转换的Bean类型 + * @param source Bean对象或Map + * @param clazz 目标的Bean类型 + * @param options 属性拷贝选项 + * @return Bean对象 + * @since 5.2.4 + */ + public static T toBean(Object source, Class clazz, CopyOptions options) { + return toBean(source, () -> ReflectUtil.newInstanceIfPossible(clazz), options); + } + + /** + * 对象或Map转Bean + * + * @param 转换的Bean类型 + * @param source Bean对象或Map + * @param targetSupplier 目标的Bean创建器 + * @param options 属性拷贝选项 + * @return Bean对象 + * @since 5.8.0 + */ + public static T toBean(Object source, Supplier targetSupplier, CopyOptions options) { + if (null == source || null == targetSupplier) { + return null; + } + final T target = targetSupplier.get(); + copyProperties(source, target, options); + return target; + } + + /** + * ServletRequest 参数转Bean + * + * @param Bean类型 + * @param beanClass Bean Class + * @param valueProvider 值提供者 + * @param copyOptions 拷贝选项,见 {@link CopyOptions} + * @return Bean + */ + public static T toBean(Class beanClass, ValueProvider valueProvider, CopyOptions copyOptions) { + if (null == beanClass || null == valueProvider) { + return null; + } + return fillBean(ReflectUtil.newInstanceIfPossible(beanClass), valueProvider, copyOptions); + } + + /** + * 填充Bean的核心方法 + * + * @param Bean类型 + * @param bean Bean + * @param valueProvider 值提供者 + * @param copyOptions 拷贝选项,见 {@link CopyOptions} + * @return Bean + */ + public static T fillBean(T bean, ValueProvider valueProvider, CopyOptions copyOptions) { + if (null == valueProvider) { + return bean; + } + + return BeanCopier.create(valueProvider, bean, copyOptions).copy(); + } + + // --------------------------------------------------------------------------------------------- beanToMap + /** + * 将bean的部分属性转换成map
+ * 可选拷贝哪些属性值,默认是不忽略值为{@code null}的值的。 + * + * @param bean bean + * @param properties 需要拷贝的属性值,{@code null}或空表示拷贝所有值 + * @return Map + * @since 5.8.0 + */ + public static Map beanToMap(Object bean, String... properties) { + int mapSize = 16; + Editor keyEditor = null; + if(ArrayUtil.isNotEmpty(properties)){ + mapSize = properties.length; + final Set propertiesSet = CollUtil.set(false, properties); + keyEditor = property -> propertiesSet.contains(property) ? property : null; + } + + // 指明了要复制的属性 所以不忽略null值 + return beanToMap(bean, new LinkedHashMap<>(mapSize, 1), false, keyEditor); + } + + /** + * 对象转Map + * + * @param bean bean对象 + * @param isToUnderlineCase 是否转换为下划线模式 + * @param ignoreNullValue 是否忽略值为空的字段 + * @return Map + */ + public static Map beanToMap(Object bean, boolean isToUnderlineCase, boolean ignoreNullValue) { + if (null == bean) { + return null; + } + return beanToMap(bean, new LinkedHashMap<>(), isToUnderlineCase, ignoreNullValue); + } + + /** + * 对象转Map + * + * @param bean bean对象 + * @param targetMap 目标的Map + * @param isToUnderlineCase 是否转换为下划线模式 + * @param ignoreNullValue 是否忽略值为空的字段 + * @return Map + * @since 3.2.3 + */ + public static Map beanToMap(Object bean, Map targetMap, final boolean isToUnderlineCase, boolean ignoreNullValue) { + if (null == bean) { + return null; + } + + return beanToMap(bean, targetMap, ignoreNullValue, key -> isToUnderlineCase ? StrUtil.toUnderlineCase(key) : key); + } + + /** + * 对象转Map
+ * 通过实现{@link Editor} 可以自定义字段值,如果这个Editor返回null则忽略这个字段,以便实现: + * + *
+	 * 1. 字段筛选,可以去除不需要的字段
+	 * 2. 字段变换,例如实现驼峰转下划线
+	 * 3. 自定义字段前缀或后缀等等
+	 * 
+ * + * @param bean bean对象 + * @param targetMap 目标的Map + * @param ignoreNullValue 是否忽略值为空的字段 + * @param keyEditor 属性字段(Map的key)编辑器,用于筛选、编辑key,如果这个Editor返回null则忽略这个字段 + * @return Map + * @since 4.0.5 + */ + public static Map beanToMap(Object bean, Map targetMap, boolean ignoreNullValue, Editor keyEditor) { + if (null == bean) { + return null; + } + + return BeanCopier.create(bean, targetMap, + CopyOptions.create() + .setIgnoreNullValue(ignoreNullValue) + .setFieldNameEditor(keyEditor) + ).copy(); + } + + /** + * 对象转Map
+ * 通过自定义{@link CopyOptions} 完成抓换选项,以便实现: + * + *
+	 * 1. 字段筛选,可以去除不需要的字段
+	 * 2. 字段变换,例如实现驼峰转下划线
+	 * 3. 自定义字段前缀或后缀等等
+	 * 4. 字段值处理
+	 * ...
+	 * 
+ * + * @param bean bean对象 + * @param targetMap 目标的Map + * @param copyOptions 拷贝选项 + * @return Map + * @since 5.7.15 + */ + public static Map beanToMap(Object bean, Map targetMap, CopyOptions copyOptions) { + if (null == bean) { + return null; + } + + return BeanCopier.create(bean, targetMap, copyOptions).copy(); + } + + // --------------------------------------------------------------------------------------------- copyProperties + + /** + * 按照Bean对象属性创建对应的Class对象,并忽略某些属性 + * + * @param 对象类型 + * @param source 源Bean对象 + * @param tClass 目标Class + * @param ignoreProperties 不拷贝的的属性列表 + * @return 目标对象 + */ + public static T copyProperties(Object source, Class tClass, String... ignoreProperties) { + if(null == source){ + return null; + } + T target = ReflectUtil.newInstanceIfPossible(tClass); + copyProperties(source, target, CopyOptions.create().setIgnoreProperties(ignoreProperties)); + return target; + } + + /** + * 复制Bean对象属性
+ * 限制类用于限制拷贝的属性,例如一个类我只想复制其父类的一些属性,就可以将editable设置为父类 + * + * @param source 源Bean对象 + * @param target 目标Bean对象 + * @param ignoreProperties 不拷贝的的属性列表 + */ + public static void copyProperties(Object source, Object target, String... ignoreProperties) { + copyProperties(source, target, CopyOptions.create().setIgnoreProperties(ignoreProperties)); + } + + /** + * 复制Bean对象属性
+ * + * @param source 源Bean对象 + * @param target 目标Bean对象 + * @param ignoreCase 是否忽略大小写 + */ + public static void copyProperties(Object source, Object target, boolean ignoreCase) { + BeanCopier.create(source, target, CopyOptions.create().setIgnoreCase(ignoreCase)).copy(); + } + + /** + * 复制Bean对象属性
+ * 限制类用于限制拷贝的属性,例如一个类我只想复制其父类的一些属性,就可以将editable设置为父类 + * + * @param source 源Bean对象 + * @param target 目标Bean对象 + * @param copyOptions 拷贝选项,见 {@link CopyOptions} + */ + public static void copyProperties(Object source, Object target, CopyOptions copyOptions) { + if(null == source){ + return; + } + BeanCopier.create(source, target, ObjectUtil.defaultIfNull(copyOptions, CopyOptions::create)).copy(); + } + + /** + * 复制集合中的Bean属性
+ * 此方法遍历集合中每个Bean,复制其属性后加入一个新的{@link List}中。 + * + * @param collection 原Bean集合 + * @param targetType 目标Bean类型 + * @param copyOptions 拷贝选项 + * @param Bean类型 + * @return 复制后的List + * @since 5.6.4 + */ + public static List copyToList(Collection collection, Class targetType, CopyOptions copyOptions) { + if (null == collection) { + return null; + } + if (collection.isEmpty()) { + return new ArrayList<>(0); + } + return collection.stream().map((source) -> { + final T target = ReflectUtil.newInstanceIfPossible(targetType); + copyProperties(source, target, copyOptions); + return target; + }).collect(Collectors.toList()); + } + + /** + * 复制集合中的Bean属性
+ * 此方法遍历集合中每个Bean,复制其属性后加入一个新的{@link List}中。 + * + * @param collection 原Bean集合 + * @param targetType 目标Bean类型 + * @param Bean类型 + * @return 复制后的List + * @since 5.6.6 + */ + public static List copyToList(Collection collection, Class targetType) { + return copyToList(collection, targetType, CopyOptions.create()); + } + + /** + * 给定的Bean的类名是否匹配指定类名字符串
+ * 如果isSimple为{@code true},则只匹配类名而忽略包名,例如:cn.hutool.TestEntity只匹配TestEntity
+ * 如果isSimple为{@code false},则匹配包括包名的全类名,例如:cn.hutool.TestEntity匹配cn.hutool.TestEntity + * + * @param bean Bean + * @param beanClassName Bean的类名 + * @param isSimple 是否只匹配类名而忽略包名,true表示忽略包名 + * @return 是否匹配 + * @since 4.0.6 + */ + public static boolean isMatchName(Object bean, String beanClassName, boolean isSimple) { + if (null == bean || StrUtil.isBlank(beanClassName)) { + return false; + } + return ClassUtil.getClassName(bean, isSimple).equals(isSimple ? StrUtil.upperFirst(beanClassName) : beanClassName); + } + + /** + * 编辑Bean的字段,static字段不会处理
+ * 例如需要对指定的字段做判空操作、null转""操作等等。 + * + * @param bean bean + * @param editor 编辑器函数 + * @param 被编辑的Bean类型 + * @return bean + * @since 5.6.4 + */ + public static T edit(T bean, Editor editor) { + if (bean == null) { + return null; + } + + final Field[] fields = ReflectUtil.getFields(bean.getClass()); + for (Field field : fields) { + if (ModifierUtil.isStatic(field)) { + continue; + } + editor.edit(field); + } + return bean; + } + + /** + * 把Bean里面的String属性做trim操作。此方法直接对传入的Bean做修改。 + *

+ * 通常bean直接用来绑定页面的input,用户的输入可能首尾存在空格,通常保存数据库前需要把首尾空格去掉 + * + * @param Bean类型 + * @param bean Bean对象 + * @param ignoreFields 不需要trim的Field名称列表(不区分大小写) + * @return 处理后的Bean对象 + */ + public static T trimStrFields(T bean, String... ignoreFields) { + return edit(bean, (field) -> { + if (ignoreFields != null && ArrayUtil.containsIgnoreCase(ignoreFields, field.getName())) { + // 不处理忽略的Fields + return field; + } + if (String.class.equals(field.getType())) { + // 只有String的Field才处理 + final String val = (String) ReflectUtil.getFieldValue(bean, field); + if (null != val) { + final String trimVal = StrUtil.trim(val); + if (!val.equals(trimVal)) { + // Field Value不为null,且首尾有空格才处理 + ReflectUtil.setFieldValue(bean, field, trimVal); + } + } + } + return field; + }); + } + + /** + * 判断Bean是否为非空对象,非空对象表示本身不为{@code null}或者含有非{@code null}属性的对象 + * + * @param bean Bean对象 + * @param ignoreFieldNames 忽略检查的字段名 + * @return 是否为非空,{@code true} - 非空 / {@code false} - 空 + * @since 5.0.7 + */ + public static boolean isNotEmpty(Object bean, String... ignoreFieldNames) { + return !isEmpty(bean, ignoreFieldNames); + } + + /** + * 判断Bean是否为空对象,空对象表示本身为{@code null}或者所有属性都为{@code null}
+ * 此方法不判断static属性 + * + * @param bean Bean对象 + * @param ignoreFieldNames 忽略检查的字段名 + * @return 是否为空,{@code true} - 空 / {@code false} - 非空 + * @since 4.1.10 + */ + public static boolean isEmpty(Object bean, String... ignoreFieldNames) { + if (null != bean) { + for (Field field : ReflectUtil.getFields(bean.getClass())) { + if (ModifierUtil.isStatic(field)) { + continue; + } + if ((!ArrayUtil.contains(ignoreFieldNames, field.getName())) + && null != ReflectUtil.getFieldValue(bean, field)) { + return false; + } + } + } + return true; + } + + /** + * 判断Bean是否包含值为{@code null}的属性
+ * 对象本身为{@code null}也返回true + * + * @param bean Bean对象 + * @param ignoreFieldNames 忽略检查的字段名 + * @return 是否包含值为null的属性,{@code true} - 包含 / {@code false} - 不包含 + * @since 4.1.10 + */ + public static boolean hasNullField(Object bean, String... ignoreFieldNames) { + if (null == bean) { + return true; + } + for (Field field : ReflectUtil.getFields(bean.getClass())) { + if (ModifierUtil.isStatic(field)) { + continue; + } + if ((!ArrayUtil.contains(ignoreFieldNames, field.getName())) + && null == ReflectUtil.getFieldValue(bean, field)) { + return true; + } + } + return false; + } + + /** + * 获取Getter或Setter方法名对应的字段名称,规则如下: + *

    + *
  • getXxxx获取为xxxx,如getName得到name。
  • + *
  • setXxxx获取为xxxx,如setName得到name。
  • + *
  • isXxxx获取为xxxx,如isName得到name。
  • + *
  • 其它不满足规则的方法名抛出{@link IllegalArgumentException}
  • + *
+ * + * @param getterOrSetterName Getter或Setter方法名 + * @return 字段名称 + * @throws IllegalArgumentException 非Getter或Setter方法 + * @since 5.7.23 + */ + public static String getFieldName(String getterOrSetterName) { + if (getterOrSetterName.startsWith("get") || getterOrSetterName.startsWith("set")) { + return StrUtil.removePreAndLowerFirst(getterOrSetterName, 3); + } else if (getterOrSetterName.startsWith("is")) { + return StrUtil.removePreAndLowerFirst(getterOrSetterName, 2); + } else { + throw new IllegalArgumentException("Invalid Getter or Setter name: " + getterOrSetterName); + } + } + + /** + * 判断source与target的所有公共字段的值是否相同 + * + * @param source 待检测对象1 + * @param target 待检测对象2 + * @param ignoreProperties 不需要检测的字段 + * @return 判断结果,如果为true则证明所有字段的值都相同 + * @since 5.8.4 + * @author Takak11 + */ + public static boolean isCommonFieldsEqual(Object source, Object target, String...ignoreProperties) { + + if (null == source && null == target) { + return true; + } + if (null == source || null == target) { + return false; + } + + Map sourceFieldsMap = BeanUtil.beanToMap(source); + Map targetFieldsMap = BeanUtil.beanToMap(target); + + Set sourceFields = sourceFieldsMap.keySet(); + sourceFields.removeAll(Arrays.asList(ignoreProperties)); + + for (String field : sourceFields) { + if(ObjectUtil.notEqual(sourceFieldsMap.get(field), targetFieldsMap.get(field))){ + return false; + } + } + + return true; + } +} diff --git a/src/main/java/cn/hutool/core/bean/DynaBean.java b/src/main/java/cn/hutool/core/bean/DynaBean.java new file mode 100644 index 0000000..a26ef6d --- /dev/null +++ b/src/main/java/cn/hutool/core/bean/DynaBean.java @@ -0,0 +1,226 @@ +package cn.hutool.core.bean; + +import cn.hutool.core.clone.CloneSupport; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.ClassUtil; +import cn.hutool.core.util.ReflectUtil; + +import java.io.Serializable; +import java.util.Map; + +/** + * 动态Bean,通过反射对Bean的相关方法做操作
+ * 支持Map和普通Bean + * + * @author Looly + * @since 3.0.7 + */ +public class DynaBean extends CloneSupport implements Serializable { + private static final long serialVersionUID = 1L; + + private final Class beanClass; + private final Object bean; + + /** + * 创建一个DynaBean + * + * @param bean 普通Bean + * @return DynaBean + */ + public static DynaBean create(Object bean) { + return new DynaBean(bean); + } + + /** + * 创建一个DynaBean + * + * @param beanClass Bean类 + * @return DynaBean + */ + public static DynaBean create(Class beanClass) { + return new DynaBean(beanClass); + } + + + /** + * 创建一个DynaBean + * + * @param beanClass Bean类 + * @param params 构造Bean所需要的参数 + * @return DynaBean + */ + public static DynaBean create(Class beanClass, Object... params) { + return new DynaBean(beanClass, params); + } + + //------------------------------------------------------------------------ Constructor start + + /** + * 构造 + * + * @param beanClass Bean类 + * @param params 构造Bean所需要的参数 + */ + public DynaBean(Class beanClass, Object... params) { + this(ReflectUtil.newInstance(beanClass, params)); + } + + /** + * 构造 + * + * @param beanClass Bean类 + */ + public DynaBean(Class beanClass) { + this(ReflectUtil.newInstance(beanClass)); + } + + /** + * 构造 + * + * @param bean 原始Bean + */ + public DynaBean(Object bean) { + Assert.notNull(bean); + if (bean instanceof DynaBean) { + bean = ((DynaBean) bean).getBean(); + } + this.bean = bean; + this.beanClass = ClassUtil.getClass(bean); + } + //------------------------------------------------------------------------ Constructor end + + /** + * 获得字段对应值 + * + * @param 属性值类型 + * @param fieldName 字段名 + * @return 字段值 + * @throws BeanException 反射获取属性值或字段值导致的异常 + */ + @SuppressWarnings("unchecked") + public T get(String fieldName) throws BeanException { + if (Map.class.isAssignableFrom(beanClass)) { + return (T) ((Map) bean).get(fieldName); + } else { + final PropDesc prop = BeanUtil.getBeanDesc(beanClass).getProp(fieldName); + if (null == prop) { + throw new BeanException("No public field or get method for {}", fieldName); + } + return (T) prop.getValue(bean); + } + } + + /** + * 检查是否有指定名称的bean属性 + * + * @param fieldName 字段名 + * @return 是否有bean属性 + * @since 5.4.2 + */ + public boolean containsProp(String fieldName) { + if (Map.class.isAssignableFrom(beanClass)) { + return ((Map) bean).containsKey(fieldName); + } else{ + return null != BeanUtil.getBeanDesc(beanClass).getProp(fieldName); + } + } + + /** + * 获得字段对应值,获取异常返回{@code null} + * + * @param 属性值类型 + * @param fieldName 字段名 + * @return 字段值 + * @since 3.1.1 + */ + public T safeGet(String fieldName) { + try { + return get(fieldName); + } catch (Exception e) { + return null; + } + } + + /** + * 设置字段值 + * + * @param fieldName 字段名 + * @param value 字段值 + * @throws BeanException 反射获取属性值或字段值导致的异常 + */ + @SuppressWarnings({"unchecked", "rawtypes"}) + public void set(String fieldName, Object value) throws BeanException { + if (Map.class.isAssignableFrom(beanClass)) { + ((Map) bean).put(fieldName, value); + } else { + final PropDesc prop = BeanUtil.getBeanDesc(beanClass).getProp(fieldName); + if (null == prop) { + throw new BeanException("No public field or set method for {}", fieldName); + } + prop.setValue(bean, value); + } + } + + /** + * 执行原始Bean中的方法 + * + * @param methodName 方法名 + * @param params 参数 + * @return 执行结果,可能为null + */ + public Object invoke(String methodName, Object... params) { + return ReflectUtil.invoke(this.bean, methodName, params); + } + + /** + * 获得原始Bean + * + * @param Bean类型 + * @return bean + */ + @SuppressWarnings("unchecked") + public T getBean() { + return (T) this.bean; + } + + /** + * 获得Bean的类型 + * + * @param Bean类型 + * @return Bean类型 + */ + @SuppressWarnings("unchecked") + public Class getBeanClass() { + return (Class) this.beanClass; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((bean == null) ? 0 : bean.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final DynaBean other = (DynaBean) obj; + if (bean == null) { + return other.bean == null; + } else return bean.equals(other.bean); + } + + @Override + public String toString() { + return this.bean.toString(); + } +} diff --git a/src/main/java/cn/hutool/core/bean/NullWrapperBean.java b/src/main/java/cn/hutool/core/bean/NullWrapperBean.java new file mode 100644 index 0000000..c94d4fa --- /dev/null +++ b/src/main/java/cn/hutool/core/bean/NullWrapperBean.java @@ -0,0 +1,29 @@ +package cn.hutool.core.bean; + +/** + * 为了解决反射过程中,需要传递null参数,但是会丢失参数类型而设立的包装类 + * + * @param Null值对应的类型 + * @author Lillls + * @since 5.5.0 + */ +public class NullWrapperBean { + + private final Class clazz; + + /** + * @param clazz null的类型 + */ + public NullWrapperBean(Class clazz) { + this.clazz = clazz; + } + + /** + * 获取null值对应的类型 + * + * @return 类型 + */ + public Class getWrappedClass() { + return clazz; + } +} diff --git a/src/main/java/cn/hutool/core/bean/PropDesc.java b/src/main/java/cn/hutool/core/bean/PropDesc.java new file mode 100644 index 0000000..30296fe --- /dev/null +++ b/src/main/java/cn/hutool/core/bean/PropDesc.java @@ -0,0 +1,403 @@ +package cn.hutool.core.bean; + +import cn.hutool.core.annotation.AnnotationUtil; +import cn.hutool.core.annotation.PropIgnore; +import cn.hutool.core.convert.Convert; +import cn.hutool.core.util.ClassUtil; +import cn.hutool.core.util.ModifierUtil; +import cn.hutool.core.util.ReflectUtil; +import cn.hutool.core.util.TypeUtil; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.Type; + +/** + * 属性描述,包括了字段、getter、setter和相应的方法执行 + * + * @author looly + */ +public class PropDesc { + + /** + * 字段 + */ + final Field field; + /** + * Getter方法 + */ + protected Method getter; + /** + * Setter方法 + */ + protected Method setter; + + /** + * 构造
+ * Getter和Setter方法设置为默认可访问 + * + * @param field 字段 + * @param getter get方法 + * @param setter set方法 + */ + public PropDesc(Field field, Method getter, Method setter) { + this.field = field; + this.getter = ClassUtil.setAccessible(getter); + this.setter = ClassUtil.setAccessible(setter); + } + + /** + * 获取字段名,如果存在Alias注解,读取注解的值作为名称 + * + * @return 字段名 + */ + public String getFieldName() { + return ReflectUtil.getFieldName(this.field); + } + + /** + * 获取字段名称 + * + * @return 字段名 + * @since 5.1.6 + */ + public String getRawFieldName() { + return null == this.field ? null : this.field.getName(); + } + + /** + * 获取字段 + * + * @return 字段 + */ + public Field getField() { + return this.field; + } + + /** + * 获得字段类型
+ * 先获取字段的类型,如果字段不存在,则获取Getter方法的返回类型,否则获取Setter的第一个参数类型 + * + * @return 字段类型 + */ + public Type getFieldType() { + if (null != this.field) { + return TypeUtil.getType(this.field); + } + return findPropType(getter, setter); + } + + /** + * 获得字段类型
+ * 先获取字段的类型,如果字段不存在,则获取Getter方法的返回类型,否则获取Setter的第一个参数类型 + * + * @return 字段类型 + */ + public Class getFieldClass() { + if (null != this.field) { + return TypeUtil.getClass(this.field); + } + return findPropClass(getter, setter); + } + + /** + * 获取Getter方法,可能为{@code null} + * + * @return Getter方法 + */ + public Method getGetter() { + return this.getter; + } + + /** + * 获取Setter方法,可能为{@code null} + * + * @return {@link Method}Setter 方法对象 + */ + public Method getSetter() { + return this.setter; + } + + /** + * 检查属性是否可读(即是否可以通过{@link #getValue(Object)}获取到值) + * + * @param checkTransient 是否检查Transient关键字或注解 + * @return 是否可读 + * @since 5.4.2 + */ + public boolean isReadable(boolean checkTransient) { + // 检查是否有getter方法或是否为public修饰 + if (null == this.getter && !ModifierUtil.isPublic(this.field)) { + return false; + } + + // 检查transient关键字和@Transient注解 + if (checkTransient && isTransientForGet()) { + return false; + } + + // 检查@PropIgnore注解 + return !isIgnoreGet(); + } + + /** + * 获取属性值
+ * 首先调用字段对应的Getter方法获取值,如果Getter方法不存在,则判断字段如果为public,则直接获取字段值
+ * 此方法不检查任何注解,使用前需调用 {@link #isReadable(boolean)} 检查是否可读 + * + * @param bean Bean对象 + * @return 字段值 + * @since 4.0.5 + */ + public Object getValue(Object bean) { + if (null != this.getter) { + return ReflectUtil.invoke(bean, this.getter); + } else if (ModifierUtil.isPublic(this.field)) { + return ReflectUtil.getFieldValue(bean, this.field); + } + + return null; + } + + /** + * 获取属性值,自动转换属性值类型
+ * 首先调用字段对应的Getter方法获取值,如果Getter方法不存在,则判断字段如果为public,则直接获取字段值 + * + * @param bean Bean对象 + * @param targetType 返回属性值需要转换的类型,null表示不转换 + * @param ignoreError 是否忽略错误,包括转换错误和注入错误 + * @return this + * @since 5.4.2 + */ + public Object getValue(Object bean, Type targetType, boolean ignoreError) { + Object result = null; + try { + result = getValue(bean); + } catch (Exception e) { + if (!ignoreError) { + throw new BeanException(e, "Get value of [{}] error!", getFieldName()); + } + } + + if (null != result && null != targetType) { + // 尝试将结果转换为目标类型,如果转换失败,返回null,即跳过此属性值。 + // 来自:issues#I41WKP@Gitee,当忽略错误情况下,目标类型转换失败应返回null + // 如果返回原值,在集合注入时会成功,但是集合取值时会报类型转换错误 + return Convert.convertWithCheck(targetType, result, null, ignoreError); + } + return result; + } + + /** + * 检查属性是否可读(即是否可以通过{@link #getValue(Object)}获取到值) + * + * @param checkTransient 是否检查Transient关键字或注解 + * @return 是否可读 + * @since 5.4.2 + */ + public boolean isWritable(boolean checkTransient) { + // 检查是否有getter方法或是否为public修饰 + if (null == this.setter && !ModifierUtil.isPublic(this.field)) { + return false; + } + + // 检查transient关键字和@Transient注解 + if (checkTransient && isTransientForSet()) { + return false; + } + + // 检查@PropIgnore注解 + return !isIgnoreSet(); + } + + /** + * 设置Bean的字段值
+ * 首先调用字段对应的Setter方法,如果Setter方法不存在,则判断字段如果为public,则直接赋值字段值
+ * 此方法不检查任何注解,使用前需调用 {@link #isWritable(boolean)} 检查是否可写 + * + * @param bean Bean对象 + * @param value 值,必须与字段值类型匹配 + * @return this + * @since 4.0.5 + */ + public PropDesc setValue(Object bean, Object value) { + if (null != this.setter) { + ReflectUtil.invoke(bean, this.setter, value); + } else if (ModifierUtil.isPublic(this.field)) { + ReflectUtil.setFieldValue(bean, this.field, value); + } + return this; + } + + /** + * 设置属性值,可以自动转换字段类型为目标类型 + * + * @param bean Bean对象 + * @param value 属性值,可以为任意类型 + * @param ignoreNull 是否忽略{@code null}值,true表示忽略 + * @param ignoreError 是否忽略错误,包括转换错误和注入错误 + * @return this + * @since 5.4.2 + */ + public PropDesc setValue(Object bean, Object value, boolean ignoreNull, boolean ignoreError) { + return setValue(bean, value, ignoreNull, ignoreError, true); + } + + /** + * 设置属性值,可以自动转换字段类型为目标类型 + * + * @param bean Bean对象 + * @param value 属性值,可以为任意类型 + * @param ignoreNull 是否忽略{@code null}值,true表示忽略 + * @param ignoreError 是否忽略错误,包括转换错误和注入错误 + * @param override 是否覆盖目标值,如果不覆盖,会先读取bean的值,{@code null}则写,否则忽略。如果覆盖,则不判断直接写 + * @return this + * @since 5.7.17 + */ + public PropDesc setValue(Object bean, Object value, boolean ignoreNull, boolean ignoreError, boolean override) { + if (null == value && ignoreNull) { + return this; + } + + // issue#I4JQ1N@Gitee + // 非覆盖模式下,如果目标值存在,则跳过 + if (!override && null != getValue(bean)) { + return this; + } + + // 当类型不匹配的时候,执行默认转换 + if (null != value) { + final Class propClass = getFieldClass(); + if (!propClass.isInstance(value)) { + value = Convert.convertWithCheck(propClass, value, null, ignoreError); + } + } + + // 属性赋值 + if (null != value || !ignoreNull) { + try { + this.setValue(bean, value); + } catch (Exception e) { + if (!ignoreError) { + throw new BeanException(e, "Set value of [{}] error!", getFieldName()); + } + // 忽略注入失败 + } + } + + return this; + } + + //------------------------------------------------------------------------------------ Private method start + + /** + * 通过Getter和Setter方法中找到属性类型 + * + * @param getter Getter方法 + * @param setter Setter方法 + * @return {@link Type} + */ + private Type findPropType(Method getter, Method setter) { + Type type = null; + if (null != getter) { + type = TypeUtil.getReturnType(getter); + } + if (null == type && null != setter) { + type = TypeUtil.getParamType(setter, 0); + } + return type; + } + + /** + * 通过Getter和Setter方法中找到属性类型 + * + * @param getter Getter方法 + * @param setter Setter方法 + * @return {@link Type} + */ + private Class findPropClass(Method getter, Method setter) { + Class type = null; + if (null != getter) { + type = TypeUtil.getReturnClass(getter); + } + if (null == type && null != setter) { + type = TypeUtil.getFirstParamClass(setter); + } + return type; + } + + /** + * 检查字段是否被忽略写,通过{@link PropIgnore} 注解完成,规则为: + *
+	 *     1. 在字段上有{@link PropIgnore} 注解
+	 *     2. 在setXXX方法上有{@link PropIgnore} 注解
+	 * 
+ * + * @return 是否忽略写 + * @since 5.4.2 + */ + private boolean isIgnoreSet() { + return AnnotationUtil.hasAnnotation(this.field, PropIgnore.class) + || AnnotationUtil.hasAnnotation(this.setter, PropIgnore.class); + } + + /** + * 检查字段是否被忽略读,通过{@link PropIgnore} 注解完成,规则为: + *
+	 *     1. 在字段上有{@link PropIgnore} 注解
+	 *     2. 在getXXX方法上有{@link PropIgnore} 注解
+	 * 
+ * + * @return 是否忽略读 + * @since 5.4.2 + */ + private boolean isIgnoreGet() { + return AnnotationUtil.hasAnnotation(this.field, PropIgnore.class) + || AnnotationUtil.hasAnnotation(this.getter, PropIgnore.class); + } + + /** + * 字段和Getter方法是否为Transient关键字修饰的 + * + * @return 是否为Transient关键字修饰的 + * @since 5.3.11 + */ + private boolean isTransientForGet() { + boolean isTransient = ModifierUtil.hasModifier(this.field, ModifierUtil.ModifierType.TRANSIENT); + + // 检查Getter方法 + if (!isTransient && null != this.getter) { + isTransient = ModifierUtil.hasModifier(this.getter, ModifierUtil.ModifierType.TRANSIENT); + + // 检查注解 + if (!isTransient) { + // isTransient = AnnotationUtil.hasAnnotation(this.getter, Transient.class); + } + } + + return isTransient; + } + + /** + * 字段和Getter方法是否为Transient关键字修饰的 + * + * @return 是否为Transient关键字修饰的 + * @since 5.3.11 + */ + private boolean isTransientForSet() { + boolean isTransient = ModifierUtil.hasModifier(this.field, ModifierUtil.ModifierType.TRANSIENT); + + // 检查Getter方法 + if (!isTransient && null != this.setter) { + isTransient = ModifierUtil.hasModifier(this.setter, ModifierUtil.ModifierType.TRANSIENT); + + // 检查注解 + if (!isTransient) { + // isTransient = AnnotationUtil.hasAnnotation(this.setter, Transient.class); + } + } + + return isTransient; + } + //------------------------------------------------------------------------------------ Private method end +} diff --git a/src/main/java/cn/hutool/core/bean/copier/AbsCopier.java b/src/main/java/cn/hutool/core/bean/copier/AbsCopier.java new file mode 100644 index 0000000..1954910 --- /dev/null +++ b/src/main/java/cn/hutool/core/bean/copier/AbsCopier.java @@ -0,0 +1,28 @@ +package cn.hutool.core.bean.copier; + +import cn.hutool.core.lang.copier.Copier; +import cn.hutool.core.util.ObjectUtil; + +/** + * 抽象的对象拷贝封装,提供来源对象、目标对象持有 + * + * @param 来源对象类型 + * @param 目标对象类型 + * @author looly + * @since 5.8.0 + */ +public abstract class AbsCopier implements Copier { + + protected final S source; + protected final T target; + /** + * 拷贝选项 + */ + protected final CopyOptions copyOptions; + + public AbsCopier(S source, T target, CopyOptions copyOptions) { + this.source = source; + this.target = target; + this.copyOptions = ObjectUtil.defaultIfNull(copyOptions, CopyOptions::create); + } +} diff --git a/src/main/java/cn/hutool/core/bean/copier/BeanCopier.java b/src/main/java/cn/hutool/core/bean/copier/BeanCopier.java new file mode 100644 index 0000000..e7a5b47 --- /dev/null +++ b/src/main/java/cn/hutool/core/bean/copier/BeanCopier.java @@ -0,0 +1,94 @@ +package cn.hutool.core.bean.copier; + +import cn.hutool.core.lang.Assert; +import cn.hutool.core.lang.copier.Copier; + +import java.io.Serializable; +import java.lang.reflect.Type; +import java.util.Map; + +/** + * Bean拷贝,提供: + * + *
+ *     1. Bean 转 Bean
+ *     2. Bean 转 Map
+ *     3. Map  转 Bean
+ *     4. Map  转 Map
+ * 
+ * + * @author looly + * + * @param 目标对象类型 + * @since 3.2.3 + */ +public class BeanCopier implements Copier, Serializable { + private static final long serialVersionUID = 1L; + + private final Copier copier; + + /** + * 创建BeanCopier + * + * @param 目标Bean类型 + * @param source 来源对象,可以是Bean或者Map + * @param target 目标Bean对象 + * @param copyOptions 拷贝属性选项 + * @return BeanCopier + */ + public static BeanCopier create(Object source, T target, CopyOptions copyOptions) { + return create(source, target, target.getClass(), copyOptions); + } + + /** + * 创建BeanCopier + * + * @param 目标Bean类型 + * @param source 来源对象,可以是Bean或者Map + * @param target 目标Bean对象 + * @param destType 目标的泛型类型,用于标注有泛型参数的Bean对象 + * @param copyOptions 拷贝属性选项 + * @return BeanCopier + */ + public static BeanCopier create(Object source, T target, Type destType, CopyOptions copyOptions) { + return new BeanCopier<>(source, target, destType, copyOptions); + } + + /** + * 构造 + * + * @param source 来源对象,可以是Bean或者Map + * @param target 目标Bean对象 + * @param targetType 目标的泛型类型,用于标注有泛型参数的Bean对象 + * @param copyOptions 拷贝属性选项 + */ + public BeanCopier(Object source, T target, Type targetType, CopyOptions copyOptions) { + Assert.notNull(source, "Source bean must be not null!"); + Assert.notNull(target, "Target bean must be not null!"); + Copier copier; + if (source instanceof Map) { + if (target instanceof Map) { + //noinspection unchecked + copier = (Copier) new MapToMapCopier((Map) source, (Map) target, targetType, copyOptions); + } else { + copier = new MapToBeanCopier<>((Map) source, target, targetType, copyOptions); + } + }else if(source instanceof ValueProvider){ + //noinspection unchecked + copier = new ValueProviderToBeanCopier<>((ValueProvider) source, target, targetType, copyOptions); + } else { + if (target instanceof Map) { + //noinspection unchecked + copier = (Copier) new BeanToMapCopier(source, (Map) target, targetType, copyOptions); + } else { + copier = new BeanToBeanCopier<>(source, target, targetType, copyOptions); + } + } + this.copier = copier; + } + + @Override + public T copy() { + return copier.copy(); + } +} diff --git a/src/main/java/cn/hutool/core/bean/copier/BeanToBeanCopier.java b/src/main/java/cn/hutool/core/bean/copier/BeanToBeanCopier.java new file mode 100644 index 0000000..ebd6e38 --- /dev/null +++ b/src/main/java/cn/hutool/core/bean/copier/BeanToBeanCopier.java @@ -0,0 +1,91 @@ +package cn.hutool.core.bean.copier; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.bean.PropDesc; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.TypeUtil; + +import java.lang.reflect.Type; +import java.util.Map; + +/** + * Bean属性拷贝到Bean中的拷贝器 + * + * @param 源Bean类型 + * @param 目标Bean类型 + * @since 5.8.0 + */ +public class BeanToBeanCopier extends AbsCopier { + + /** + * 目标的类型(用于泛型类注入) + */ + private final Type targetType; + + /** + * 构造 + * + * @param source 来源Map + * @param target 目标Bean对象 + * @param targetType 目标泛型类型 + * @param copyOptions 拷贝选项 + */ + public BeanToBeanCopier(S source, T target, Type targetType, CopyOptions copyOptions) { + super(source, target, copyOptions); + this.targetType = targetType; + } + + @Override + public T copy() { + Class actualEditable = target.getClass(); + if (null != copyOptions.editable) { + // 检查限制类是否为target的父类或接口 + Assert.isTrue(copyOptions.editable.isInstance(target), + "Target class [{}] not assignable to Editable class [{}]", actualEditable.getName(), copyOptions.editable.getName()); + actualEditable = copyOptions.editable; + } + final Map targetPropDescMap = BeanUtil.getBeanDesc(actualEditable).getPropMap(copyOptions.ignoreCase); + + final Map sourcePropDescMap = BeanUtil.getBeanDesc(source.getClass()).getPropMap(copyOptions.ignoreCase); + sourcePropDescMap.forEach((sFieldName, sDesc) -> { + if (null == sFieldName || !sDesc.isReadable(copyOptions.transientSupport)) { + // 字段空或不可读,跳过 + return; + } + + sFieldName = copyOptions.editFieldName(sFieldName); + // 对key做转换,转换后为null的跳过 + if (null == sFieldName) { + return; + } + + // 忽略不需要拷贝的 key, + if (!copyOptions.testKeyFilter(sFieldName)) { + return; + } + + // 检查目标字段可写性 + final PropDesc tDesc = targetPropDescMap.get(sFieldName); + if (null == tDesc || !tDesc.isWritable(this.copyOptions.transientSupport)) { + // 字段不可写,跳过之 + return; + } + + // 检查源对象属性是否过滤属性 + Object sValue = sDesc.getValue(this.source); + if (!copyOptions.testPropertyFilter(sDesc.getField(), sValue)) { + return; + } + + // 获取目标字段真实类型并转换源值 + final Type fieldType = TypeUtil.getActualType(this.targetType, tDesc.getFieldType()); + //sValue = Convert.convertWithCheck(fieldType, sValue, null, this.copyOptions.ignoreError); + sValue = this.copyOptions.convertField(fieldType, sValue); + sValue = copyOptions.editFieldValue(sFieldName, sValue); + + // 目标赋值 + tDesc.setValue(this.target, sValue, copyOptions.ignoreNullValue, copyOptions.ignoreError, copyOptions.override); + }); + return this.target; + } +} diff --git a/src/main/java/cn/hutool/core/bean/copier/BeanToMapCopier.java b/src/main/java/cn/hutool/core/bean/copier/BeanToMapCopier.java new file mode 100644 index 0000000..7b1c4c8 --- /dev/null +++ b/src/main/java/cn/hutool/core/bean/copier/BeanToMapCopier.java @@ -0,0 +1,87 @@ +package cn.hutool.core.bean.copier; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.bean.PropDesc; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.TypeUtil; + +import java.lang.reflect.Type; +import java.util.Map; + +/** + * Bean属性拷贝到Map中的拷贝器 + * + * @since 5.8.0 + */ +@SuppressWarnings("rawtypes") +public class BeanToMapCopier extends AbsCopier { + + /** + * 目标的Map类型(用于泛型类注入) + */ + private final Type targetType; + + /** + * 构造 + * + * @param source 来源Map + * @param target 目标Map对象 + * @param targetType 目标泛型类型 + * @param copyOptions 拷贝选项 + */ + public BeanToMapCopier(Object source, Map target, Type targetType, CopyOptions copyOptions) { + super(source, target, copyOptions); + this.targetType = targetType; + } + + @Override + public Map copy() { + Class actualEditable = source.getClass(); + if (null != copyOptions.editable) { + // 检查限制类是否为target的父类或接口 + Assert.isTrue(copyOptions.editable.isInstance(source), + "Source class [{}] not assignable to Editable class [{}]", actualEditable.getName(), copyOptions.editable.getName()); + actualEditable = copyOptions.editable; + } + + final Map sourcePropDescMap = BeanUtil.getBeanDesc(actualEditable).getPropMap(copyOptions.ignoreCase); + sourcePropDescMap.forEach((sFieldName, sDesc) -> { + if (null == sFieldName || !sDesc.isReadable(copyOptions.transientSupport)) { + // 字段空或不可读,跳过 + return; + } + + sFieldName = copyOptions.editFieldName(sFieldName); + // 对key做转换,转换后为null的跳过 + if (null == sFieldName) { + return; + } + + // 忽略不需要拷贝的 key, + if (!copyOptions.testKeyFilter(sFieldName)) { + return; + } + + // 检查源对象属性是否过滤属性 + Object sValue = sDesc.getValue(this.source); + if (!copyOptions.testPropertyFilter(sDesc.getField(), sValue)) { + return; + } + + // 获取目标值真实类型并转换源值 + final Type[] typeArguments = TypeUtil.getTypeArguments(this.targetType); + if(null != typeArguments){ + //sValue = Convert.convertWithCheck(typeArguments[1], sValue, null, this.copyOptions.ignoreError); + sValue = this.copyOptions.convertField(typeArguments[1], sValue); + sValue = copyOptions.editFieldValue(sFieldName, sValue); + } + + // 目标赋值 + if(null != sValue || !copyOptions.ignoreNullValue){ + //noinspection unchecked + target.put(sFieldName, sValue); + } + }); + return this.target; + } +} diff --git a/src/main/java/cn/hutool/core/bean/copier/CopyOptions.java b/src/main/java/cn/hutool/core/bean/copier/CopyOptions.java new file mode 100644 index 0000000..cdeea06 --- /dev/null +++ b/src/main/java/cn/hutool/core/bean/copier/CopyOptions.java @@ -0,0 +1,383 @@ +package cn.hutool.core.bean.copier; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.convert.Convert; +import cn.hutool.core.convert.TypeConverter; +import cn.hutool.core.lang.Editor; +import cn.hutool.core.lang.func.Func1; +import cn.hutool.core.lang.func.LambdaUtil; +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.ReflectUtil; + +import java.io.Serializable; +import java.lang.reflect.Field; +import java.lang.reflect.Type; +import java.util.Map; +import java.util.Set; +import java.util.function.BiFunction; +import java.util.function.BiPredicate; + +/** + * 属性拷贝选项
+ * 包括:
+ * 1、限制的类或接口,必须为目标对象的实现接口或父类,用于限制拷贝的属性,例如一个类我只想复制其父类的一些属性,就可以将editable设置为父类
+ * 2、是否忽略空值,当源对象的值为null时,true: 忽略而不注入此值,false: 注入null
+ * 3、忽略的属性列表,设置一个属性列表,不拷贝这些属性值
+ * + * @author Looly + */ +public class CopyOptions implements Serializable { + private static final long serialVersionUID = 1L; + + /** + * 限制的类或接口,必须为目标对象的实现接口或父类,用于限制拷贝的属性,例如一个类我只想复制其父类的一些属性,就可以将editable设置为父类
+ * 如果目标对象是Map,源对象是Bean,则作用于源对象上 + */ + protected Class editable; + /** + * 是否忽略空值,当源对象的值为null时,true: 忽略而不注入此值,false: 注入null + */ + protected boolean ignoreNullValue; + /** + * 属性过滤器,断言通过的属性才会被复制
+ * 断言参数中Field为源对象的字段对象,如果源对象为Map,使用目标对象,Object为源对象的对应值 + */ + private BiPredicate propertiesFilter; + /** + * 是否忽略字段注入错误 + */ + protected boolean ignoreError; + /** + * 是否忽略字段大小写 + */ + protected boolean ignoreCase; + /** + * 字段属性编辑器,用于自定义属性转换规则,例如驼峰转下划线等
+ * 规则为,{@link Editor#edit(Object)}属性为源对象的字段名称或key,返回值为目标对象的字段名称或key + */ + private Editor fieldNameEditor; + /** + * 字段属性值编辑器,用于自定义属性值转换规则,例如null转""等 + */ + protected BiFunction fieldValueEditor; + /** + * 是否支持transient关键字修饰和@Transient注解,如果支持,被修饰的字段或方法对应的字段将被忽略。 + */ + protected boolean transientSupport = true; + /** + * 是否覆盖目标值,如果不覆盖,会先读取目标对象的值,非{@code null}则写,否则忽略。如果覆盖,则不判断直接写 + */ + protected boolean override = true; + + /** + * 源对象和目标对象都是 {@code Map} 时, 需要忽略的源对象 {@code Map} key + */ + private Set ignoreKeySet; + + /** + * 自定义类型转换器,默认使用全局万能转换器转换 + */ + protected TypeConverter converter = (type, value) -> { + if(null == value){ + return null; + } + + final String name = value.getClass().getName(); + if(ArrayUtil.contains(new String[]{"cn.hutool.json.JSONObject", "cn.hutool.json.JSONArray"}, name)){ + // 由于设计缺陷导致JSON转Bean时无法使用自定义的反序列化器,此处采用反射方式修复bug,此类问题会在6.x解决 + return ReflectUtil.invoke(value, "toBean", ObjectUtil.defaultIfNull(type, Object.class)); + } + + return Convert.convertWithCheck(type, value, null, ignoreError); + }; + + //region create + + /** + * 创建拷贝选项 + * + * @return 拷贝选项 + */ + public static CopyOptions create() { + return new CopyOptions(); + } + + /** + * 创建拷贝选项 + * + * @param editable 限制的类或接口,必须为目标对象的实现接口或父类,用于限制拷贝的属性 + * @param ignoreNullValue 是否忽略空值,当源对象的值为null时,true: 忽略而不注入此值,false: 注入null + * @param ignoreProperties 忽略的属性列表,设置一个属性列表,不拷贝这些属性值 + * @return 拷贝选项 + */ + public static CopyOptions create(Class editable, boolean ignoreNullValue, String... ignoreProperties) { + return new CopyOptions(editable, ignoreNullValue, ignoreProperties); + } + //endregion + + /** + * 构造拷贝选项 + */ + public CopyOptions() { + } + + /** + * 构造拷贝选项 + * + * @param editable 限制的类或接口,必须为目标对象的实现接口或父类,用于限制拷贝的属性 + * @param ignoreNullValue 是否忽略空值,当源对象的值为null时,true: 忽略而不注入此值,false: 注入null + * @param ignoreProperties 忽略的目标对象中属性列表,设置一个属性列表,不拷贝这些属性值 + */ + public CopyOptions(Class editable, boolean ignoreNullValue, String... ignoreProperties) { + this.propertiesFilter = (f, v) -> true; + this.editable = editable; + this.ignoreNullValue = ignoreNullValue; + this.setIgnoreProperties(ignoreProperties); + } + + /** + * 设置限制的类或接口,必须为目标对象的实现接口或父类,用于限制拷贝的属性 + * + * @param editable 限制的类或接口 + * @return CopyOptions + */ + public CopyOptions setEditable(Class editable) { + this.editable = editable; + return this; + } + + /** + * 设置是否忽略空值,当源对象的值为null时,true: 忽略而不注入此值,false: 注入null + * + * @param ignoreNullVall 是否忽略空值,当源对象的值为null时,true: 忽略而不注入此值,false: 注入null + * @return CopyOptions + */ + public CopyOptions setIgnoreNullValue(boolean ignoreNullVall) { + this.ignoreNullValue = ignoreNullVall; + return this; + } + + /** + * 设置忽略空值,当源对象的值为null时,忽略而不注入此值 + * + * @return CopyOptions + * @since 4.5.7 + */ + public CopyOptions ignoreNullValue() { + return setIgnoreNullValue(true); + } + + /** + * 属性过滤器,断言通过的属性才会被复制
+ * {@link BiPredicate#test(Object, Object)}返回{@code true}则属性通过,{@code false}不通过,抛弃之 + * + * @param propertiesFilter 属性过滤器 + * @return CopyOptions + */ + public CopyOptions setPropertiesFilter(BiPredicate propertiesFilter) { + this.propertiesFilter = propertiesFilter; + return this; + } + + /** + * 设置忽略的目标对象中属性列表,设置一个属性列表,不拷贝这些属性值 + * + * @param ignoreProperties 忽略的目标对象中属性列表,设置一个属性列表,不拷贝这些属性值 + * @return CopyOptions + */ + public CopyOptions setIgnoreProperties(String... ignoreProperties) { + this.ignoreKeySet = CollUtil.newHashSet(ignoreProperties); + return this; + } + + /** + * 设置忽略的目标对象中属性列表,设置一个属性列表,不拷贝这些属性值,Lambda方式 + * + * @param

参数类型 + * @param 返回值类型 + * @param funcs 忽略的目标对象中属性列表,设置一个属性列表,不拷贝这些属性值 + * @return CopyOptions + * @since 5.8.0 + */ + @SuppressWarnings("unchecked") + public CopyOptions setIgnoreProperties(Func1... funcs) { + this.ignoreKeySet = ArrayUtil.mapToSet(funcs, LambdaUtil::getFieldName); + return this; + } + + /** + * 设置是否忽略字段的注入错误 + * + * @param ignoreError 是否忽略注入错误 + * @return CopyOptions + */ + public CopyOptions setIgnoreError(boolean ignoreError) { + this.ignoreError = ignoreError; + return this; + } + + /** + * 设置忽略字段的注入错误 + * + * @return CopyOptions + * @since 4.5.7 + */ + public CopyOptions ignoreError() { + return setIgnoreError(true); + } + + /** + * 设置是否忽略字段的大小写 + * + * @param ignoreCase 是否忽略大小写 + * @return CopyOptions + */ + public CopyOptions setIgnoreCase(boolean ignoreCase) { + this.ignoreCase = ignoreCase; + return this; + } + + /** + * 设置忽略字段的大小写 + * + * @return CopyOptions + * @since 4.5.7 + */ + public CopyOptions ignoreCase() { + return setIgnoreCase(true); + } + + /** + * 设置拷贝属性的字段映射,用于不同的属性之前拷贝做对应表用
+ * 需要注意的是,当使用ValueProvider作为数据提供者时,这个映射是相反的,即fieldMapping中key为目标Bean的名称,而value是提供者中的key + * + * @param fieldMapping 拷贝属性的字段映射,用于不同的属性之前拷贝做对应表用 + * @return CopyOptions + */ + public CopyOptions setFieldMapping(Map fieldMapping) { + return setFieldNameEditor((key -> fieldMapping.getOrDefault(key, key))); + } + + /** + * 设置字段属性编辑器,用于自定义属性转换规则,例如驼峰转下划线等
+ * 此转换器只针对源端的字段做转换,请确认转换后与目标端字段一致
+ * 当转换后的字段名为null时忽略这个字段
+ * 需要注意的是,当使用ValueProvider作为数据提供者时,这个映射是相反的,即fieldMapping中key为目标Bean的名称,而value是提供者中的key + * + * @param fieldNameEditor 字段属性编辑器,用于自定义属性转换规则,例如驼峰转下划线等 + * @return CopyOptions + * @since 5.4.2 + */ + public CopyOptions setFieldNameEditor(Editor fieldNameEditor) { + this.fieldNameEditor = fieldNameEditor; + return this; + } + + /** + * 设置字段属性值编辑器,用于自定义属性值转换规则,例如null转""等
+ * + * @param fieldValueEditor 字段属性值编辑器,用于自定义属性值转换规则,例如null转""等 + * @return CopyOptions + * @since 5.7.15 + */ + public CopyOptions setFieldValueEditor(BiFunction fieldValueEditor) { + this.fieldValueEditor = fieldValueEditor; + return this; + } + + /** + * 编辑字段值 + * + * @param fieldName 字段名 + * @param fieldValue 字段值 + * @return 编辑后的字段值 + * @since 5.7.15 + */ + protected Object editFieldValue(String fieldName, Object fieldValue) { + return (null != this.fieldValueEditor) ? + this.fieldValueEditor.apply(fieldName, fieldValue) : fieldValue; + } + + /** + * 设置是否支持transient关键字修饰和@Transient注解,如果支持,被修饰的字段或方法对应的字段将被忽略。 + * + * @param transientSupport 是否支持 + * @return this + * @since 5.4.2 + */ + public CopyOptions setTransientSupport(boolean transientSupport) { + this.transientSupport = transientSupport; + return this; + } + + /** + * 设置是否覆盖目标值,如果不覆盖,会先读取目标对象的值,为{@code null}则写,否则忽略。如果覆盖,则不判断直接写 + * + * @param override 是否覆盖目标值 + * @return this + * @since 5.7.17 + */ + public CopyOptions setOverride(boolean override) { + this.override = override; + return this; + } + + /** + * 设置自定义类型转换器,默认使用全局万能转换器转换。 + * + * @param converter 转换器 + * @return this + * @since 5.8.0 + */ + public CopyOptions setConverter(TypeConverter converter) { + this.converter = converter; + return this; + } + + /** + * 使用自定义转换器转换字段值
+ * 如果自定义转换器为{@code null},则返回原值。 + * + * @param targetType 目标类型 + * @param fieldValue 字段值 + * @return 编辑后的字段值 + * @since 5.8.0 + */ + protected Object convertField(Type targetType, Object fieldValue) { + return (null != this.converter) ? + this.converter.convert(targetType, fieldValue) : fieldValue; + } + + /** + * 转换字段名为编辑后的字段名 + * + * @param fieldName 字段名 + * @return 编辑后的字段名 + * @since 5.4.2 + */ + protected String editFieldName(String fieldName) { + return (null != this.fieldNameEditor) ? this.fieldNameEditor.edit(fieldName) : fieldName; + } + + /** + * 测试是否保留字段,{@code true}保留,{@code false}不保留 + * + * @param field 字段 + * @param value 值 + * @return 是否保留 + */ + protected boolean testPropertyFilter(Field field, Object value) { + return null == this.propertiesFilter || this.propertiesFilter.test(field, value); + } + + /** + * 测试是否保留key, {@code true} 不保留, {@code false} 保留 + * + * @param key {@link Map} key + * @return 是否保留 + */ + protected boolean testKeyFilter(Object key) { + return CollUtil.isEmpty(this.ignoreKeySet) || !this.ignoreKeySet.contains(key); + } +} diff --git a/src/main/java/cn/hutool/core/bean/copier/MapToBeanCopier.java b/src/main/java/cn/hutool/core/bean/copier/MapToBeanCopier.java new file mode 100644 index 0000000..948e5f6 --- /dev/null +++ b/src/main/java/cn/hutool/core/bean/copier/MapToBeanCopier.java @@ -0,0 +1,119 @@ +package cn.hutool.core.bean.copier; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.bean.PropDesc; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.map.CaseInsensitiveMap; +import cn.hutool.core.map.MapWrapper; +import cn.hutool.core.util.StrUtil; +import cn.hutool.core.util.TypeUtil; + +import java.lang.reflect.Type; +import java.util.Map; + +/** + * Map属性拷贝到Bean中的拷贝器 + * + * @param 目标Bean类型 + * @since 5.8.0 + */ +public class MapToBeanCopier extends AbsCopier, T> { + + /** + * 目标的类型(用于泛型类注入) + */ + private final Type targetType; + + /** + * 构造 + * + * @param source 来源Map + * @param target 目标Bean对象 + * @param targetType 目标泛型类型 + * @param copyOptions 拷贝选项 + */ + public MapToBeanCopier(Map source, T target, Type targetType, CopyOptions copyOptions) { + super(source, target, copyOptions); + + // 针对MapWrapper特殊处理,提供的Map包装了忽略大小写的Map,则默认转Bean的时候也忽略大小写,如JSONObject + if(source instanceof MapWrapper){ + final Map raw = ((MapWrapper) source).getRaw(); + if(raw instanceof CaseInsensitiveMap){ + copyOptions.setIgnoreCase(true); + } + } + + this.targetType = targetType; + } + + @Override + public T copy() { + Class actualEditable = target.getClass(); + if (null != copyOptions.editable) { + // 检查限制类是否为target的父类或接口 + Assert.isTrue(copyOptions.editable.isInstance(target), + "Target class [{}] not assignable to Editable class [{}]", actualEditable.getName(), copyOptions.editable.getName()); + actualEditable = copyOptions.editable; + } + final Map targetPropDescMap = BeanUtil.getBeanDesc(actualEditable).getPropMap(copyOptions.ignoreCase); + + this.source.forEach((sKey, sValue) -> { + if (null == sKey) { + return; + } + String sKeyStr = copyOptions.editFieldName(sKey.toString()); + // 对key做转换,转换后为null的跳过 + if (null == sKeyStr) { + return; + } + + // 忽略不需要拷贝的 key, + if (!copyOptions.testKeyFilter(sKeyStr)) { + return; + } + + // 检查目标字段可写性 + final PropDesc tDesc = findPropDesc(targetPropDescMap, sKeyStr); + if (null == tDesc || !tDesc.isWritable(this.copyOptions.transientSupport)) { + // 字段不可写,跳过之 + return; + } + sKeyStr = tDesc.getFieldName(); + + // 检查目标是否过滤属性 + if (!copyOptions.testPropertyFilter(tDesc.getField(), sValue)) { + return; + } + + // 获取目标字段真实类型并转换源值 + final Type fieldType = TypeUtil.getActualType(this.targetType, tDesc.getFieldType()); + //Object newValue = Convert.convertWithCheck(fieldType, sValue, null, this.copyOptions.ignoreError); + Object newValue = this.copyOptions.convertField(fieldType, sValue); + newValue = copyOptions.editFieldValue(sKeyStr, newValue); + + // 目标赋值 + tDesc.setValue(this.target, newValue, copyOptions.ignoreNullValue, copyOptions.ignoreError, copyOptions.override); + }); + return this.target; + } + + /** + * 查找Map对应Bean的名称
+ * 尝试原名称、转驼峰名称、isXxx去掉is的名称 + * + * @param targetPropDescMap 目标bean的属性描述Map + * @param sKeyStr 键或字段名 + * @return {@link PropDesc} + */ + private PropDesc findPropDesc(Map targetPropDescMap, String sKeyStr){ + PropDesc propDesc = targetPropDescMap.get(sKeyStr); + if(null != propDesc){ + return propDesc; + } + + // 转驼峰尝试查找 + sKeyStr = StrUtil.toCamelCase(sKeyStr); + propDesc = targetPropDescMap.get(sKeyStr); + return propDesc; + } +} diff --git a/src/main/java/cn/hutool/core/bean/copier/MapToMapCopier.java b/src/main/java/cn/hutool/core/bean/copier/MapToMapCopier.java new file mode 100644 index 0000000..701f5ea --- /dev/null +++ b/src/main/java/cn/hutool/core/bean/copier/MapToMapCopier.java @@ -0,0 +1,75 @@ +package cn.hutool.core.bean.copier; + +import cn.hutool.core.util.TypeUtil; + +import java.lang.reflect.Type; +import java.util.Map; + +/** + * Map属性拷贝到Map中的拷贝器 + * + * @since 5.8.0 + */ +@SuppressWarnings({"rawtypes", "unchecked"}) +public class MapToMapCopier extends AbsCopier { + + /** + * 目标的类型(用于泛型类注入) + */ + private final Type targetType; + + /** + * 构造 + * + * @param source 来源Map + * @param target 目标Bean对象 + * @param targetType 目标泛型类型 + * @param copyOptions 拷贝选项 + */ + public MapToMapCopier(Map source, Map target, Type targetType, CopyOptions copyOptions) { + super(source, target, copyOptions); + this.targetType = targetType; + } + + @Override + public Map copy() { + this.source.forEach((sKey, sValue) -> { + if (null == sKey) { + return; + } + // 忽略空值 + if (copyOptions.ignoreNullValue && sValue == null) { + return; + } + + final String sKeyStr = copyOptions.editFieldName(sKey.toString()); + // 对key做转换,转换后为null的跳过 + if (null == sKeyStr) { + return; + } + + // 忽略不需要拷贝的 key, + if (!copyOptions.testKeyFilter(sKeyStr)) { + return; + } + + final Object targetValue = target.get(sKeyStr); + // 非覆盖模式下,如果目标值存在,则跳过 + if (!copyOptions.override && null != targetValue) { + return; + } + + // 获取目标值真实类型并转换源值 + final Type[] typeArguments = TypeUtil.getTypeArguments(this.targetType); + if (null != typeArguments) { + //sValue = Convert.convertWithCheck(typeArguments[1], sValue, null, this.copyOptions.ignoreError); + sValue = this.copyOptions.convertField(typeArguments[1], sValue); + sValue = copyOptions.editFieldValue(sKeyStr, sValue); + } + + // 目标赋值 + target.put(sKeyStr, sValue); + }); + return this.target; + } +} diff --git a/src/main/java/cn/hutool/core/bean/copier/ValueProvider.java b/src/main/java/cn/hutool/core/bean/copier/ValueProvider.java new file mode 100644 index 0000000..9f96116 --- /dev/null +++ b/src/main/java/cn/hutool/core/bean/copier/ValueProvider.java @@ -0,0 +1,34 @@ +package cn.hutool.core.bean.copier; + +import java.lang.reflect.Type; + +/** + * 值提供者,用于提供Bean注入时参数对应值得抽象接口
+ * 继承或匿名实例化此接口
+ * 在Bean注入过程中,Bean获得字段名,通过外部方式根据这个字段名查找相应的字段值,然后注入Bean
+ * + * @author Looly + * @param KEY类型,一般情况下为 {@link String} + * + */ +public interface ValueProvider{ + + /** + * 获取值
+ * 返回值一般需要匹配被注入类型,如果不匹配会调用默认转换 Convert#convert(Type, Object)实现转换 + * + * @param key Bean对象中参数名 + * @param valueType 被注入的值的类型 + * @return 对应参数名的值 + */ + Object value(T key, Type valueType); + + /** + * 是否包含指定KEY,如果不包含则忽略注入
+ * 此接口方法单独需要实现的意义在于:有些值提供者(比如Map)key是存在的,但是value为null,此时如果需要注入这个null,需要根据此方法判断 + * + * @param key Bean对象中参数名 + * @return 是否包含指定KEY + */ + boolean containsKey(T key); +} diff --git a/src/main/java/cn/hutool/core/bean/copier/ValueProviderToBeanCopier.java b/src/main/java/cn/hutool/core/bean/copier/ValueProviderToBeanCopier.java new file mode 100644 index 0000000..3a2bdfb --- /dev/null +++ b/src/main/java/cn/hutool/core/bean/copier/ValueProviderToBeanCopier.java @@ -0,0 +1,89 @@ +package cn.hutool.core.bean.copier; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.bean.PropDesc; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.TypeUtil; + +import java.lang.reflect.Type; +import java.util.Map; + +/** + * {@link ValueProvider}属性拷贝到Bean中的拷贝器 + * + * @param 目标Bean类型 + * @since 5.8.0 + */ +public class ValueProviderToBeanCopier extends AbsCopier, T> { + + /** + * 目标的类型(用于泛型类注入) + */ + private final Type targetType; + + /** + * 构造 + * + * @param source 来源Map + * @param target 目标Bean对象 + * @param targetType 目标泛型类型 + * @param copyOptions 拷贝选项 + */ + public ValueProviderToBeanCopier(ValueProvider source, T target, Type targetType, CopyOptions copyOptions) { + super(source, target, copyOptions); + this.targetType = targetType; + } + + @Override + public T copy() { + Class actualEditable = target.getClass(); + if (null != copyOptions.editable) { + // 检查限制类是否为target的父类或接口 + Assert.isTrue(copyOptions.editable.isInstance(target), + "Target class [{}] not assignable to Editable class [{}]", actualEditable.getName(), copyOptions.editable.getName()); + actualEditable = copyOptions.editable; + } + final Map targetPropDescMap = BeanUtil.getBeanDesc(actualEditable).getPropMap(copyOptions.ignoreCase); + + targetPropDescMap.forEach((tFieldName, tDesc) -> { + if (null == tFieldName) { + return; + } + tFieldName = copyOptions.editFieldName(tFieldName); + // 对key做转换,转换后为null的跳过 + if (null == tFieldName) { + return; + } + + // 无字段内容跳过 + if(!source.containsKey(tFieldName)){ + return; + } + + // 忽略不需要拷贝的 key, + if (!copyOptions.testKeyFilter(tFieldName)) { + return; + } + + // 检查目标字段可写性 + if (null == tDesc || !tDesc.isWritable(this.copyOptions.transientSupport)) { + // 字段不可写,跳过之 + return; + } + + // 获取目标字段真实类型 + final Type fieldType = TypeUtil.getActualType(this.targetType ,tDesc.getFieldType()); + + // 检查目标对象属性是否过滤属性 + Object sValue = source.value(tFieldName, fieldType); + if (!copyOptions.testPropertyFilter(tDesc.getField(), sValue)) { + return; + } + sValue = copyOptions.editFieldValue(tFieldName, sValue); + + // 目标赋值 + tDesc.setValue(this.target, sValue, copyOptions.ignoreNullValue, copyOptions.ignoreError, copyOptions.override); + }); + return this.target; + } +} diff --git a/src/main/java/cn/hutool/core/bean/copier/package-info.java b/src/main/java/cn/hutool/core/bean/copier/package-info.java new file mode 100644 index 0000000..6220095 --- /dev/null +++ b/src/main/java/cn/hutool/core/bean/copier/package-info.java @@ -0,0 +1,7 @@ +/** + * Bean拷贝实现,包括拷贝选项等 + * + * @author looly + * + */ +package cn.hutool.core.bean.copier; \ No newline at end of file diff --git a/src/main/java/cn/hutool/core/bean/copier/provider/BeanValueProvider.java b/src/main/java/cn/hutool/core/bean/copier/provider/BeanValueProvider.java new file mode 100644 index 0000000..4040322 --- /dev/null +++ b/src/main/java/cn/hutool/core/bean/copier/provider/BeanValueProvider.java @@ -0,0 +1,100 @@ +package cn.hutool.core.bean.copier.provider; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.bean.PropDesc; +import cn.hutool.core.bean.copier.ValueProvider; +import cn.hutool.core.lang.Editor; +import cn.hutool.core.map.FuncKeyMap; +import cn.hutool.core.util.StrUtil; + +import java.io.Serializable; +import java.lang.reflect.Type; +import java.util.HashMap; +import java.util.Map; +import java.util.function.Function; + +/** + * Bean的值提供者 + * + * @author looly + */ +public class BeanValueProvider implements ValueProvider { + + private final Object source; + private final boolean ignoreError; + final Map sourcePdMap; + + /** + * 构造 + * + * @param bean Bean + * @param ignoreCase 是否忽略字段大小写 + * @param ignoreError 是否忽略字段值读取错误 + */ + public BeanValueProvider(Object bean, boolean ignoreCase, boolean ignoreError) { + this(bean, ignoreCase, ignoreError, null); + } + + /** + * 构造 + * + * @param bean Bean + * @param ignoreCase 是否忽略字段大小写 + * @param ignoreError 是否忽略字段值读取错误 + * @param keyEditor 键编辑器 + */ + public BeanValueProvider(Object bean, boolean ignoreCase, boolean ignoreError, Editor keyEditor) { + this.source = bean; + this.ignoreError = ignoreError; + final Map sourcePdMap = BeanUtil.getBeanDesc(source.getClass()).getPropMap(ignoreCase); + // issue#2202@Github + // 如果用户定义了键编辑器,则提供的map中的数据必须全部转换key + // issue#I5VRHW@Gitee 使Function可以被序列化 + this.sourcePdMap = new FuncKeyMap<>(new HashMap<>(sourcePdMap.size(), 1), (Function & Serializable)(key) -> { + if (ignoreCase && key instanceof CharSequence) { + key = key.toString().toLowerCase(); + } + if (null != keyEditor) { + key = keyEditor.edit(key.toString()); + } + return key.toString(); + }); + this.sourcePdMap.putAll(sourcePdMap); + } + + @Override + public Object value(String key, Type valueType) { + final PropDesc sourcePd = getPropDesc(key, valueType); + + Object result = null; + if (null != sourcePd) { + result = sourcePd.getValue(this.source, valueType, this.ignoreError); + } + return result; + } + + @Override + public boolean containsKey(String key) { + final PropDesc sourcePd = getPropDesc(key, null); + + // 字段描述不存在或忽略读的情况下,表示不存在 + return null != sourcePd && sourcePd.isReadable(false); + } + + /** + * 获得属性描述 + * + * @param key 字段名 + * @param valueType 值类型,用于判断是否为Boolean,可以为null + * @return 属性描述 + */ + private PropDesc getPropDesc(String key, Type valueType) { + PropDesc sourcePd = sourcePdMap.get(key); + if (null == sourcePd && (null == valueType || Boolean.class == valueType || boolean.class == valueType)) { + //boolean类型字段字段名支持两种方式 + sourcePd = sourcePdMap.get(StrUtil.upperFirstAndAddPre(key, "is")); + } + + return sourcePd; + } +} diff --git a/src/main/java/cn/hutool/core/bean/copier/provider/DynaBeanValueProvider.java b/src/main/java/cn/hutool/core/bean/copier/provider/DynaBeanValueProvider.java new file mode 100644 index 0000000..74e34c3 --- /dev/null +++ b/src/main/java/cn/hutool/core/bean/copier/provider/DynaBeanValueProvider.java @@ -0,0 +1,42 @@ +package cn.hutool.core.bean.copier.provider; + +import cn.hutool.core.bean.DynaBean; +import cn.hutool.core.bean.copier.ValueProvider; +import cn.hutool.core.convert.Convert; + +import java.lang.reflect.Type; + +/** + * DynaBean值提供者 + * + * @author looly + * @since 5.4.2 + */ +public class DynaBeanValueProvider implements ValueProvider { + + private final DynaBean dynaBean; + private final boolean ignoreError; + + /** + * 构造 + * + * @param dynaBean DynaBean + * @param ignoreError 是否忽略错误 + */ + public DynaBeanValueProvider(DynaBean dynaBean, boolean ignoreError) { + this.dynaBean = dynaBean; + this.ignoreError = ignoreError; + } + + @Override + public Object value(String key, Type valueType) { + final Object value = dynaBean.get(key); + return Convert.convertWithCheck(valueType, value, null, this.ignoreError); + } + + @Override + public boolean containsKey(String key) { + return dynaBean.containsProp(key); + } + +} diff --git a/src/main/java/cn/hutool/core/bean/copier/provider/package-info.java b/src/main/java/cn/hutool/core/bean/copier/provider/package-info.java new file mode 100644 index 0000000..f52e7bc --- /dev/null +++ b/src/main/java/cn/hutool/core/bean/copier/provider/package-info.java @@ -0,0 +1,7 @@ +/** + * Bean值提供者方式封装 + * + * @author looly + * + */ +package cn.hutool.core.bean.copier.provider; \ No newline at end of file diff --git a/src/main/java/cn/hutool/core/bean/package-info.java b/src/main/java/cn/hutool/core/bean/package-info.java new file mode 100644 index 0000000..dc27ffd --- /dev/null +++ b/src/main/java/cn/hutool/core/bean/package-info.java @@ -0,0 +1,7 @@ +/** + * Bean相关操作,包括Bean信息描述,Bean路径表达式、动态Bean、Bean工具等 + * + * @author looly + * + */ +package cn.hutool.core.bean; \ No newline at end of file diff --git a/src/main/java/cn/hutool/core/builder/Builder.java b/src/main/java/cn/hutool/core/builder/Builder.java new file mode 100644 index 0000000..518ac8d --- /dev/null +++ b/src/main/java/cn/hutool/core/builder/Builder.java @@ -0,0 +1,19 @@ +package cn.hutool.core.builder; + +import java.io.Serializable; + +/** + * 建造者模式接口定义 + * + * @param 建造对象类型 + * @author Looly + * @since 4.2.2 + */ +public interface Builder extends Serializable{ + /** + * 构建 + * + * @return 被构建的对象 + */ + T build(); +} \ No newline at end of file diff --git a/src/main/java/cn/hutool/core/builder/CompareToBuilder.java b/src/main/java/cn/hutool/core/builder/CompareToBuilder.java new file mode 100644 index 0000000..5c82ffd --- /dev/null +++ b/src/main/java/cn/hutool/core/builder/CompareToBuilder.java @@ -0,0 +1,975 @@ +package cn.hutool.core.builder; + +import cn.hutool.core.util.ArrayUtil; + +import java.lang.reflect.AccessibleObject; +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.Collection; +import java.util.Comparator; + +/** + * 用于构建 {@link Comparable#compareTo(Object)} 方法的辅助工具 + * + *

+ * 在Bean对象中,所有相关字段都参与比对,继承的字段不参与。使用方法如下: + * + *

+ * public class MyClass {
+ *   String field1;
+ *   int field2;
+ *   boolean field3;
+ *
+ *   ...
+ *
+ *   public int compareTo(Object o) {
+ *     MyClass myClass = (MyClass) o;
+ *     return new CompareToBuilder()
+ *       .appendSuper(super.compareTo(o)
+ *       .append(this.field1, myClass.field1)
+ *       .append(this.field2, myClass.field2)
+ *       .append(this.field3, myClass.field3)
+ *       .toComparison();
+ *   }
+ * }
+ * 
+ * + * 字段值按照顺序比较,如果某个字段返回非0结果,比较终止,使用{@code toComparison()}返回结果,后续比较忽略。 + * + *

+ * 也可以使用{@link #reflectionCompare(Object, Object) reflectionCompare} 方法通过反射比较字段,使用方法如下: + * + *

+ * public int compareTo(Object o) {
+ *   return CompareToBuilder.reflectionCompare(this, o);
+ * }
+ * 
+ * + *TODO 待整理 + * 来自于Apache-Commons-Lang3 + * @author looly,Apache-Commons + * @since 4.2.2 + */ +public class CompareToBuilder implements Builder { + private static final long serialVersionUID = 1L; + + /** 当前比较状态 */ + private int comparison; + + /** + * 构造,构造后调用append方法增加比较项,然后调用{@link #toComparison()}获取结果 + */ + public CompareToBuilder() { + comparison = 0; + } + + //----------------------------------------------------------------------- + /** + * 通过反射比较两个Bean对象,对象字段可以为private。比较规则如下: + * + *
    + *
  • static字段不比较
  • + *
  • Transient字段不参与比较
  • + *
  • 父类字段参与比较
  • + *
+ * + *

+ *如果被比较的两个对象都为null,被认为相同。 + * + * @param lhs 第一个对象 + * @param rhs 第二个对象 + * @return a negative integer, zero, or a positive integer as lhs + * is less than, equal to, or greater than rhs + * @throws NullPointerException if either (but not both) parameters are + * null + * @throws ClassCastException if rhs is not assignment-compatible + * with lhs + */ + public static int reflectionCompare(final Object lhs, final Object rhs) { + return reflectionCompare(lhs, rhs, false, null); + } + + /** + *

Compares two Objects via reflection.

+ * + *

Fields can be private, thus AccessibleObject.setAccessible + * is used to bypass normal access control checks. This will fail under a + * security manager unless the appropriate permissions are set.

+ * + *
    + *
  • Static fields will not be compared
  • + *
  • If compareTransients is true, + * compares transient members. Otherwise ignores them, as they + * are likely derived fields.
  • + *
  • Superclass fields will be compared
  • + *
+ * + *

If both lhs and rhs are null, + * they are considered equal.

+ * + * @param lhs left-hand object + * @param rhs right-hand object + * @param compareTransients whether to compare transient fields + * @return a negative integer, zero, or a positive integer as lhs + * is less than, equal to, or greater than rhs + * @throws NullPointerException if either lhs or rhs + * (but not both) is null + * @throws ClassCastException if rhs is not assignment-compatible + * with lhs + */ + public static int reflectionCompare(final Object lhs, final Object rhs, final boolean compareTransients) { + return reflectionCompare(lhs, rhs, compareTransients, null); + } + + /** + *

Compares two Objects via reflection.

+ * + *

Fields can be private, thus AccessibleObject.setAccessible + * is used to bypass normal access control checks. This will fail under a + * security manager unless the appropriate permissions are set.

+ * + *
    + *
  • Static fields will not be compared
  • + *
  • If compareTransients is true, + * compares transient members. Otherwise ignores them, as they + * are likely derived fields.
  • + *
  • Superclass fields will be compared
  • + *
+ * + *

If both lhs and rhs are null, + * they are considered equal.

+ * + * @param lhs left-hand object + * @param rhs right-hand object + * @param excludeFields Collection of String fields to exclude + * @return a negative integer, zero, or a positive integer as lhs + * is less than, equal to, or greater than rhs + * @throws NullPointerException if either lhs or rhs + * (but not both) is null + * @throws ClassCastException if rhs is not assignment-compatible + * with lhs + * @since 2.2 + */ + public static int reflectionCompare(final Object lhs, final Object rhs, final Collection excludeFields) { + return reflectionCompare(lhs, rhs, ArrayUtil.toArray(excludeFields, String.class)); + } + + /** + *

Compares two Objects via reflection.

+ * + *

Fields can be private, thus AccessibleObject.setAccessible + * is used to bypass normal access control checks. This will fail under a + * security manager unless the appropriate permissions are set.

+ * + *
    + *
  • Static fields will not be compared
  • + *
  • If compareTransients is true, + * compares transient members. Otherwise ignores them, as they + * are likely derived fields.
  • + *
  • Superclass fields will be compared
  • + *
+ * + *

If both lhs and rhs are null, + * they are considered equal.

+ * + * @param lhs left-hand object + * @param rhs right-hand object + * @param excludeFields array of fields to exclude + * @return a negative integer, zero, or a positive integer as lhs + * is less than, equal to, or greater than rhs + * @throws NullPointerException if either lhs or rhs + * (but not both) is null + * @throws ClassCastException if rhs is not assignment-compatible + * with lhs + * @since 2.2 + */ + public static int reflectionCompare(final Object lhs, final Object rhs, final String... excludeFields) { + return reflectionCompare(lhs, rhs, false, null, excludeFields); + } + + /** + *

Compares two Objects via reflection.

+ * + *

Fields can be private, thus AccessibleObject.setAccessible + * is used to bypass normal access control checks. This will fail under a + * security manager unless the appropriate permissions are set.

+ * + *
    + *
  • Static fields will not be compared
  • + *
  • If the compareTransients is true, + * compares transient members. Otherwise ignores them, as they + * are likely derived fields.
  • + *
  • Compares superclass fields up to and including reflectUpToClass. + * If reflectUpToClass is null, compares all superclass fields.
  • + *
+ * + *

If both lhs and rhs are null, + * they are considered equal.

+ * + * @param lhs left-hand object + * @param rhs right-hand object + * @param compareTransients whether to compare transient fields + * @param reflectUpToClass last superclass for which fields are compared + * @param excludeFields fields to exclude + * @return a negative integer, zero, or a positive integer as lhs + * is less than, equal to, or greater than rhs + * @throws NullPointerException if either lhs or rhs + * (but not both) is null + * @throws ClassCastException if rhs is not assignment-compatible + * with lhs + * @since 2.2 (2.0 as reflectionCompare(Object, Object, boolean, Class)) + */ + public static int reflectionCompare( + final Object lhs, + final Object rhs, + final boolean compareTransients, + final Class reflectUpToClass, + final String... excludeFields) { + + if (lhs == rhs) { + return 0; + } + if (lhs == null || rhs == null) { + throw new NullPointerException(); + } + Class lhsClazz = lhs.getClass(); + if (!lhsClazz.isInstance(rhs)) { + throw new ClassCastException(); + } + final CompareToBuilder compareToBuilder = new CompareToBuilder(); + reflectionAppend(lhs, rhs, lhsClazz, compareToBuilder, compareTransients, excludeFields); + while (lhsClazz.getSuperclass() != null && lhsClazz != reflectUpToClass) { + lhsClazz = lhsClazz.getSuperclass(); + reflectionAppend(lhs, rhs, lhsClazz, compareToBuilder, compareTransients, excludeFields); + } + return compareToBuilder.toComparison(); + } + + /** + *

Appends to builder the comparison of lhs + * to rhs using the fields defined in clazz.

+ * + * @param lhs left-hand object + * @param rhs right-hand object + * @param clazz Class that defines fields to be compared + * @param builder CompareToBuilder to append to + * @param useTransients whether to compare transient fields + * @param excludeFields fields to exclude + */ + private static void reflectionAppend( + final Object lhs, + final Object rhs, + final Class clazz, + final CompareToBuilder builder, + final boolean useTransients, + final String[] excludeFields) { + + final Field[] fields = clazz.getDeclaredFields(); + AccessibleObject.setAccessible(fields, true); + for (int i = 0; i < fields.length && builder.comparison == 0; i++) { + final Field f = fields[i]; + if (!ArrayUtil.contains(excludeFields, f.getName()) + && (f.getName().indexOf('$') == -1) + && (useTransients || !Modifier.isTransient(f.getModifiers())) + && (!Modifier.isStatic(f.getModifiers()))) { + try { + builder.append(f.get(lhs), f.get(rhs)); + } catch (final IllegalAccessException e) { + // This can't happen. Would get a Security exception instead. + // Throw a runtime exception in case the impossible happens. + throw new InternalError("Unexpected IllegalAccessException"); + } + } + } + } + + //----------------------------------------------------------------------- + /** + *

Appends to the builder the compareTo(Object) + * result of the superclass.

+ * + * @param superCompareTo result of calling super.compareTo(Object) + * @return this - used to chain append calls + * @since 2.0 + */ + public CompareToBuilder appendSuper(final int superCompareTo) { + if (comparison != 0) { + return this; + } + comparison = superCompareTo; + return this; + } + + //----------------------------------------------------------------------- + /** + *

Appends to the builder the comparison of + * two Objects.

+ * + *
    + *
  1. Check if lhs == rhs
  2. + *
  3. Check if either lhs or rhs is null, + * a null object is less than a non-null object
  4. + *
  5. Check the object contents
  6. + *
+ * + *

lhs must either be an array or implement {@link Comparable}.

+ * + * @param lhs left-hand object + * @param rhs right-hand object + * @return this - used to chain append calls + * @throws ClassCastException if rhs is not assignment-compatible + * with lhs + */ + public CompareToBuilder append(final Object lhs, final Object rhs) { + return append(lhs, rhs, null); + } + + /** + *

Appends to the builder the comparison of + * two Objects.

+ * + *
    + *
  1. Check if lhs == rhs
  2. + *
  3. Check if either lhs or rhs is null, + * a null object is less than a non-null object
  4. + *
  5. Check the object contents
  6. + *
+ * + *

If lhs is an array, array comparison methods will be used. + * Otherwise comparator will be used to compare the objects. + * If comparator is null, lhs must + * implement {@link Comparable} instead.

+ * + * @param lhs left-hand object + * @param rhs right-hand object + * @param comparator Comparator used to compare the objects, + * null means treat lhs as Comparable + * @return this - used to chain append calls + * @throws ClassCastException if rhs is not assignment-compatible + * with lhs + * @since 2.0 + */ + public CompareToBuilder append(final Object lhs, final Object rhs, final Comparator comparator) { + if (comparison != 0) { + return this; + } + if (lhs == rhs) { + return this; + } + if (lhs == null) { + comparison = -1; + return this; + } + if (rhs == null) { + comparison = +1; + return this; + } + if (lhs.getClass().isArray()) { + // switch on type of array, to dispatch to the correct handler + // handles multi dimensional arrays + // throws a ClassCastException if rhs is not the correct array type + if (lhs instanceof long[]) { + append((long[]) lhs, (long[]) rhs); + } else if (lhs instanceof int[]) { + append((int[]) lhs, (int[]) rhs); + } else if (lhs instanceof short[]) { + append((short[]) lhs, (short[]) rhs); + } else if (lhs instanceof char[]) { + append((char[]) lhs, (char[]) rhs); + } else if (lhs instanceof byte[]) { + append((byte[]) lhs, (byte[]) rhs); + } else if (lhs instanceof double[]) { + append((double[]) lhs, (double[]) rhs); + } else if (lhs instanceof float[]) { + append((float[]) lhs, (float[]) rhs); + } else if (lhs instanceof boolean[]) { + append((boolean[]) lhs, (boolean[]) rhs); + } else { + // not an array of primitives + // throws a ClassCastException if rhs is not an array + append((Object[]) lhs, (Object[]) rhs, comparator); + } + } else { + // the simple case, not an array, just test the element + if (comparator == null) { + @SuppressWarnings("unchecked") // assume this can be done; if not throw CCE as per Javadoc + final Comparable comparable = (Comparable) lhs; + comparison = comparable.compareTo(rhs); + } else { + @SuppressWarnings("unchecked") // assume this can be done; if not throw CCE as per Javadoc + final Comparator comparator2 = (Comparator) comparator; + comparison = comparator2.compare(lhs, rhs); + } + } + return this; + } + + //------------------------------------------------------------------------- + /** + * Appends to the builder the comparison of + * two longs. + * + * @param lhs left-hand value + * @param rhs right-hand value + * @return this - used to chain append calls + */ + public CompareToBuilder append(final long lhs, final long rhs) { + if (comparison != 0) { + return this; + } + comparison = (Long.compare(lhs, rhs)); + return this; + } + + /** + * Appends to the builder the comparison of + * two ints. + * + * @param lhs left-hand value + * @param rhs right-hand value + * @return this - used to chain append calls + */ + public CompareToBuilder append(final int lhs, final int rhs) { + if (comparison != 0) { + return this; + } + comparison = (Integer.compare(lhs, rhs)); + return this; + } + + /** + * Appends to the builder the comparison of + * two shorts. + * + * @param lhs left-hand value + * @param rhs right-hand value + * @return this - used to chain append calls + */ + public CompareToBuilder append(final short lhs, final short rhs) { + if (comparison != 0) { + return this; + } + comparison = (Short.compare(lhs, rhs)); + return this; + } + + /** + * Appends to the builder the comparison of + * two chars. + * + * @param lhs left-hand value + * @param rhs right-hand value + * @return this - used to chain append calls + */ + public CompareToBuilder append(final char lhs, final char rhs) { + if (comparison != 0) { + return this; + } + comparison = (Character.compare(lhs, rhs)); + return this; + } + + /** + * Appends to the builder the comparison of + * two bytes. + * + * @param lhs left-hand value + * @param rhs right-hand value + * @return this - used to chain append calls + */ + public CompareToBuilder append(final byte lhs, final byte rhs) { + if (comparison != 0) { + return this; + } + comparison = (Byte.compare(lhs, rhs)); + return this; + } + + /** + *

Appends to the builder the comparison of + * two doubles.

+ * + *

This handles NaNs, Infinities, and -0.0.

+ * + *

It is compatible with the hash code generated by + * HashCodeBuilder.

+ * + * @param lhs left-hand value + * @param rhs right-hand value + * @return this - used to chain append calls + */ + public CompareToBuilder append(final double lhs, final double rhs) { + if (comparison != 0) { + return this; + } + comparison = Double.compare(lhs, rhs); + return this; + } + + /** + *

Appends to the builder the comparison of + * two floats.

+ * + *

This handles NaNs, Infinities, and -0.0.

+ * + *

It is compatible with the hash code generated by + * HashCodeBuilder.

+ * + * @param lhs left-hand value + * @param rhs right-hand value + * @return this - used to chain append calls + */ + public CompareToBuilder append(final float lhs, final float rhs) { + if (comparison != 0) { + return this; + } + comparison = Float.compare(lhs, rhs); + return this; + } + + /** + * Appends to the builder the comparison of + * two booleanss. + * + * @param lhs left-hand value + * @param rhs right-hand value + * @return this - used to chain append calls + */ + public CompareToBuilder append(final boolean lhs, final boolean rhs) { + if (comparison != 0) { + return this; + } + if (lhs == rhs) { + return this; + } + if (!lhs) { + comparison = -1; + } else { + comparison = +1; + } + return this; + } + + //----------------------------------------------------------------------- + /** + *

Appends to the builder the deep comparison of + * two Object arrays.

+ * + *
    + *
  1. Check if arrays are the same using ==
  2. + *
  3. Check if for null, null is less than non-null
  4. + *
  5. Check array length, a short length array is less than a long length array
  6. + *
  7. Check array contents element by element using {@link #append(Object, Object, Comparator)}
  8. + *
+ * + *

This method will also will be called for the top level of multi-dimensional, + * ragged, and multi-typed arrays.

+ * + * @param lhs left-hand array + * @param rhs right-hand array + * @return this - used to chain append calls + * @throws ClassCastException if rhs is not assignment-compatible + * with lhs + */ + public CompareToBuilder append(final Object[] lhs, final Object[] rhs) { + return append(lhs, rhs, null); + } + + /** + *

Appends to the builder the deep comparison of + * two Object arrays.

+ * + *
    + *
  1. Check if arrays are the same using ==
  2. + *
  3. Check if for null, null is less than non-null
  4. + *
  5. Check array length, a short length array is less than a long length array
  6. + *
  7. Check array contents element by element using {@link #append(Object, Object, Comparator)}
  8. + *
+ * + *

This method will also will be called for the top level of multi-dimensional, + * ragged, and multi-typed arrays.

+ * + * @param lhs left-hand array + * @param rhs right-hand array + * @param comparator Comparator to use to compare the array elements, + * null means to treat lhs elements as Comparable. + * @return this - used to chain append calls + * @throws ClassCastException if rhs is not assignment-compatible + * with lhs + * @since 2.0 + */ + public CompareToBuilder append(final Object[] lhs, final Object[] rhs, final Comparator comparator) { + if (comparison != 0) { + return this; + } + if (lhs == rhs) { + return this; + } + if (lhs == null) { + comparison = -1; + return this; + } + if (rhs == null) { + comparison = +1; + return this; + } + if (lhs.length != rhs.length) { + comparison = (lhs.length < rhs.length) ? -1 : +1; + return this; + } + for (int i = 0; i < lhs.length && comparison == 0; i++) { + append(lhs[i], rhs[i], comparator); + } + return this; + } + + /** + *

Appends to the builder the deep comparison of + * two long arrays.

+ * + *
    + *
  1. Check if arrays are the same using ==
  2. + *
  3. Check if for null, null is less than non-null
  4. + *
  5. Check array length, a shorter length array is less than a longer length array
  6. + *
  7. Check array contents element by element using {@link #append(long, long)}
  8. + *
+ * + * @param lhs left-hand array + * @param rhs right-hand array + * @return this - used to chain append calls + */ + public CompareToBuilder append(final long[] lhs, final long[] rhs) { + if (comparison != 0) { + return this; + } + if (lhs == rhs) { + return this; + } + if (lhs == null) { + comparison = -1; + return this; + } + if (rhs == null) { + comparison = +1; + return this; + } + if (lhs.length != rhs.length) { + comparison = (lhs.length < rhs.length) ? -1 : +1; + return this; + } + for (int i = 0; i < lhs.length && comparison == 0; i++) { + append(lhs[i], rhs[i]); + } + return this; + } + + /** + *

Appends to the builder the deep comparison of + * two int arrays.

+ * + *
    + *
  1. Check if arrays are the same using ==
  2. + *
  3. Check if for null, null is less than non-null
  4. + *
  5. Check array length, a shorter length array is less than a longer length array
  6. + *
  7. Check array contents element by element using {@link #append(int, int)}
  8. + *
+ * + * @param lhs left-hand array + * @param rhs right-hand array + * @return this - used to chain append calls + */ + public CompareToBuilder append(final int[] lhs, final int[] rhs) { + if (comparison != 0) { + return this; + } + if (lhs == rhs) { + return this; + } + if (lhs == null) { + comparison = -1; + return this; + } + if (rhs == null) { + comparison = +1; + return this; + } + if (lhs.length != rhs.length) { + comparison = (lhs.length < rhs.length) ? -1 : +1; + return this; + } + for (int i = 0; i < lhs.length && comparison == 0; i++) { + append(lhs[i], rhs[i]); + } + return this; + } + + /** + *

Appends to the builder the deep comparison of + * two short arrays.

+ * + *
    + *
  1. Check if arrays are the same using ==
  2. + *
  3. Check if for null, null is less than non-null
  4. + *
  5. Check array length, a shorter length array is less than a longer length array
  6. + *
  7. Check array contents element by element using {@link #append(short, short)}
  8. + *
+ * + * @param lhs left-hand array + * @param rhs right-hand array + * @return this - used to chain append calls + */ + public CompareToBuilder append(final short[] lhs, final short[] rhs) { + if (comparison != 0) { + return this; + } + if (lhs == rhs) { + return this; + } + if (lhs == null) { + comparison = -1; + return this; + } + if (rhs == null) { + comparison = +1; + return this; + } + if (lhs.length != rhs.length) { + comparison = (lhs.length < rhs.length) ? -1 : +1; + return this; + } + for (int i = 0; i < lhs.length && comparison == 0; i++) { + append(lhs[i], rhs[i]); + } + return this; + } + + /** + *

Appends to the builder the deep comparison of + * two char arrays.

+ * + *
    + *
  1. Check if arrays are the same using ==
  2. + *
  3. Check if for null, null is less than non-null
  4. + *
  5. Check array length, a shorter length array is less than a longer length array
  6. + *
  7. Check array contents element by element using {@link #append(char, char)}
  8. + *
+ * + * @param lhs left-hand array + * @param rhs right-hand array + * @return this - used to chain append calls + */ + public CompareToBuilder append(final char[] lhs, final char[] rhs) { + if (comparison != 0) { + return this; + } + if (lhs == rhs) { + return this; + } + if (lhs == null) { + comparison = -1; + return this; + } + if (rhs == null) { + comparison = +1; + return this; + } + if (lhs.length != rhs.length) { + comparison = (lhs.length < rhs.length) ? -1 : +1; + return this; + } + for (int i = 0; i < lhs.length && comparison == 0; i++) { + append(lhs[i], rhs[i]); + } + return this; + } + + /** + *

Appends to the builder the deep comparison of + * two byte arrays.

+ * + *
    + *
  1. Check if arrays are the same using ==
  2. + *
  3. Check if for null, null is less than non-null
  4. + *
  5. Check array length, a shorter length array is less than a longer length array
  6. + *
  7. Check array contents element by element using {@link #append(byte, byte)}
  8. + *
+ * + * @param lhs left-hand array + * @param rhs right-hand array + * @return this - used to chain append calls + */ + public CompareToBuilder append(final byte[] lhs, final byte[] rhs) { + if (comparison != 0) { + return this; + } + if (lhs == rhs) { + return this; + } + if (lhs == null) { + comparison = -1; + return this; + } + if (rhs == null) { + comparison = +1; + return this; + } + if (lhs.length != rhs.length) { + comparison = (lhs.length < rhs.length) ? -1 : +1; + return this; + } + for (int i = 0; i < lhs.length && comparison == 0; i++) { + append(lhs[i], rhs[i]); + } + return this; + } + + /** + *

Appends to the builder the deep comparison of + * two double arrays.

+ * + *
    + *
  1. Check if arrays are the same using ==
  2. + *
  3. Check if for null, null is less than non-null
  4. + *
  5. Check array length, a shorter length array is less than a longer length array
  6. + *
  7. Check array contents element by element using {@link #append(double, double)}
  8. + *
+ * + * @param lhs left-hand array + * @param rhs right-hand array + * @return this - used to chain append calls + */ + public CompareToBuilder append(final double[] lhs, final double[] rhs) { + if (comparison != 0) { + return this; + } + if (lhs == rhs) { + return this; + } + if (lhs == null) { + comparison = -1; + return this; + } + if (rhs == null) { + comparison = +1; + return this; + } + if (lhs.length != rhs.length) { + comparison = (lhs.length < rhs.length) ? -1 : +1; + return this; + } + for (int i = 0; i < lhs.length && comparison == 0; i++) { + append(lhs[i], rhs[i]); + } + return this; + } + + /** + *

Appends to the builder the deep comparison of + * two float arrays.

+ * + *
    + *
  1. Check if arrays are the same using ==
  2. + *
  3. Check if for null, null is less than non-null
  4. + *
  5. Check array length, a shorter length array is less than a longer length array
  6. + *
  7. Check array contents element by element using {@link #append(float, float)}
  8. + *
+ * + * @param lhs left-hand array + * @param rhs right-hand array + * @return this - used to chain append calls + */ + public CompareToBuilder append(final float[] lhs, final float[] rhs) { + if (comparison != 0) { + return this; + } + if (lhs == rhs) { + return this; + } + if (lhs == null) { + comparison = -1; + return this; + } + if (rhs == null) { + comparison = +1; + return this; + } + if (lhs.length != rhs.length) { + comparison = (lhs.length < rhs.length) ? -1 : +1; + return this; + } + for (int i = 0; i < lhs.length && comparison == 0; i++) { + append(lhs[i], rhs[i]); + } + return this; + } + + /** + *

Appends to the builder the deep comparison of + * two boolean arrays.

+ * + *
    + *
  1. Check if arrays are the same using ==
  2. + *
  3. Check if for null, null is less than non-null
  4. + *
  5. Check array length, a shorter length array is less than a longer length array
  6. + *
  7. Check array contents element by element using {@link #append(boolean, boolean)}
  8. + *
+ * + * @param lhs left-hand array + * @param rhs right-hand array + * @return this - used to chain append calls + */ + public CompareToBuilder append(final boolean[] lhs, final boolean[] rhs) { + if (comparison != 0) { + return this; + } + if (lhs == rhs) { + return this; + } + if (lhs == null) { + comparison = -1; + return this; + } + if (rhs == null) { + comparison = +1; + return this; + } + if (lhs.length != rhs.length) { + comparison = (lhs.length < rhs.length) ? -1 : +1; + return this; + } + for (int i = 0; i < lhs.length && comparison == 0; i++) { + append(lhs[i], rhs[i]); + } + return this; + } + + //----------------------------------------------------------------------- + /** + * Returns a negative integer, a positive integer, or zero as + * the builder has judged the "left-hand" side + * as less than, greater than, or equal to the "right-hand" + * side. + * + * @return final comparison result + * @see #build() + */ + public int toComparison() { + return comparison; + } + + /** + * Returns a negative Integer, a positive Integer, or zero as + * the builder has judged the "left-hand" side + * as less than, greater than, or equal to the "right-hand" + * side. + * + * @return final comparison result as an Integer + * @see #toComparison() + * @since 3.0 + */ + @Override + public Integer build() { + return toComparison(); + } +} + diff --git a/src/main/java/cn/hutool/core/builder/EqualsBuilder.java b/src/main/java/cn/hutool/core/builder/EqualsBuilder.java new file mode 100644 index 0000000..c3ac2e8 --- /dev/null +++ b/src/main/java/cn/hutool/core/builder/EqualsBuilder.java @@ -0,0 +1,563 @@ +package cn.hutool.core.builder; + +import cn.hutool.core.lang.Pair; +import cn.hutool.core.util.ArrayUtil; + +import java.lang.reflect.AccessibleObject; +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; + +/** + *

{@link Object#equals(Object)} 方法的构建器

+ * + *

两个对象equals必须保证hashCode值相等,hashCode值相等不能保证一定equals

+ * + *

使用方法如下:

+ *
+ * public boolean equals(Object obj) {
+ *   if (obj == null) { return false; }
+ *   if (obj == this) { return true; }
+ *   if (obj.getClass() != getClass()) {
+ *     return false;
+ *   }
+ *   MyClass rhs = (MyClass) obj;
+ *   return new EqualsBuilder()
+ *                 .appendSuper(super.equals(obj))
+ *                 .append(field1, rhs.field1)
+ *                 .append(field2, rhs.field2)
+ *                 .append(field3, rhs.field3)
+ *                 .isEquals();
+ *  }
+ * 
+ * + *

我们也可以通过反射判断所有字段是否equals:

+ *
+ * public boolean equals(Object obj) {
+ *   return EqualsBuilder.reflectionEquals(this, obj);
+ * }
+ * 
+ *

+ * 来自Apache Commons Lang改造 + */ +public class EqualsBuilder implements Builder { + private static final long serialVersionUID = 1L; + + /** + *

+ * A registry of objects used by reflection methods to detect cyclical object references and avoid infinite loops. + *

+ */ + private static final ThreadLocal>> REGISTRY = new ThreadLocal<>(); + + /** + *

+ * Returns the registry of object pairs being traversed by the reflection + * methods in the current thread. + *

+ * + * @return Set the registry of objects being traversed + * @since 3.0 + */ + static Set> getRegistry() { + return REGISTRY.get(); + } + + /** + *

+ * Converters value pair into a register pair. + *

+ * + * @param lhs {@code this} object + * @param rhs the other object + * @return the pair + */ + static Pair getRegisterPair(final Object lhs, final Object rhs) { + final IDKey left = new IDKey(lhs); + final IDKey right = new IDKey(rhs); + return new Pair<>(left, right); + } + + /** + *

+ * Returns {@code true} if the registry contains the given object pair. + * Used by the reflection methods to avoid infinite loops. + * Objects might be swapped therefore a check is needed if the object pair + * is registered in given or swapped order. + *

+ * + * @param lhs {@code this} object to lookup in registry + * @param rhs the other object to lookup on registry + * @return boolean {@code true} if the registry contains the given object. + * @since 3.0 + */ + static boolean isRegistered(final Object lhs, final Object rhs) { + final Set> registry = getRegistry(); + final Pair pair = getRegisterPair(lhs, rhs); + final Pair swappedPair = new Pair<>(pair.getKey(), pair.getValue()); + + return registry != null + && (registry.contains(pair) || registry.contains(swappedPair)); + } + + /** + *

+ * Registers the given object pair. + * Used by the reflection methods to avoid infinite loops. + *

+ * + * @param lhs {@code this} object to register + * @param rhs the other object to register + */ + static void register(final Object lhs, final Object rhs) { + synchronized (EqualsBuilder.class) { + if (getRegistry() == null) { + REGISTRY.set(new HashSet<>()); + } + } + + final Set> registry = getRegistry(); + final Pair pair = getRegisterPair(lhs, rhs); + registry.add(pair); + } + + /** + *

+ * Unregisters the given object pair. + *

+ * + *

+ * Used by the reflection methods to avoid infinite loops. + * + * @param lhs {@code this} object to unregister + * @param rhs the other object to unregister + * @since 3.0 + */ + static void unregister(final Object lhs, final Object rhs) { + Set> registry = getRegistry(); + if (registry != null) { + final Pair pair = getRegisterPair(lhs, rhs); + registry.remove(pair); + synchronized (EqualsBuilder.class) { + //read again + registry = getRegistry(); + if (registry != null && registry.isEmpty()) { + REGISTRY.remove(); + } + } + } + } + + /** + * 是否equals,此值随着构建会变更,默认true + */ + private boolean isEquals = true; + + /** + * 构造,初始状态值为true + */ + public EqualsBuilder() { + // do nothing for now. + } + + //------------------------------------------------------------------------- + + /** + *

反射检查两个对象是否equals,此方法检查对象及其父对象的属性(包括私有属性)是否equals

+ * + * @param lhs 此对象 + * @param rhs 另一个对象 + * @param excludeFields 排除的字段集合,如果有不参与计算equals的字段加入此集合即可 + * @return 两个对象是否equals,是返回{@code true} + */ + public static boolean reflectionEquals(final Object lhs, final Object rhs, final Collection excludeFields) { + return reflectionEquals(lhs, rhs, ArrayUtil.toArray(excludeFields, String.class)); + } + + /** + *

反射检查两个对象是否equals,此方法检查对象及其父对象的属性(包括私有属性)是否equals

+ * + * @param lhs 此对象 + * @param rhs 另一个对象 + * @param excludeFields 排除的字段集合,如果有不参与计算equals的字段加入此集合即可 + * @return 两个对象是否equals,是返回{@code true} + */ + public static boolean reflectionEquals(final Object lhs, final Object rhs, final String... excludeFields) { + return reflectionEquals(lhs, rhs, false, null, excludeFields); + } + + /** + *

This method uses reflection to determine if the two {@code Object}s + * are equal.

+ * + *

It uses {@code AccessibleObject.setAccessible} to gain access to private + * fields. This means that it will throw a security exception if run under + * a security manager, if the permissions are not set up correctly. It is also + * not as efficient as testing explicitly. Non-primitive fields are compared using + * {@code equals()}.

+ * + *

If the TestTransients parameter is set to {@code true}, transient + * members will be tested, otherwise they are ignored, as they are likely + * derived fields, and not part of the value of the {@code Object}.

+ * + *

Static fields will not be tested. Superclass fields will be included.

+ * + * @param lhs {@code this} object + * @param rhs the other object + * @param testTransients whether to include transient fields + * @return {@code true} if the two Objects have tested equals. + */ + public static boolean reflectionEquals(final Object lhs, final Object rhs, final boolean testTransients) { + return reflectionEquals(lhs, rhs, testTransients, null); + } + + /** + *

This method uses reflection to determine if the two {@code Object}s + * are equal.

+ * + *

It uses {@code AccessibleObject.setAccessible} to gain access to private + * fields. This means that it will throw a security exception if run under + * a security manager, if the permissions are not set up correctly. It is also + * not as efficient as testing explicitly. Non-primitive fields are compared using + * {@code equals()}.

+ * + *

If the testTransients parameter is set to {@code true}, transient + * members will be tested, otherwise they are ignored, as they are likely + * derived fields, and not part of the value of the {@code Object}.

+ * + *

Static fields will not be included. Superclass fields will be appended + * up to and including the specified superclass. A null superclass is treated + * as java.lang.Object.

+ * + * @param lhs {@code this} object + * @param rhs the other object + * @param testTransients whether to include transient fields + * @param reflectUpToClass the superclass to reflect up to (inclusive), + * may be {@code null} + * @param excludeFields array of field names to exclude from testing + * @return {@code true} if the two Objects have tested equals. + * @since 2.0 + */ + public static boolean reflectionEquals(final Object lhs, final Object rhs, final boolean testTransients, final Class reflectUpToClass, + final String... excludeFields) { + if (lhs == rhs) { + return true; + } + if (lhs == null || rhs == null) { + return false; + } + // Find the leaf class since there may be transients in the leaf + // class or in classes between the leaf and root. + // If we are not testing transients or a subclass has no ivars, + // then a subclass can test equals to a superclass. + final Class lhsClass = lhs.getClass(); + final Class rhsClass = rhs.getClass(); + Class testClass; + if (lhsClass.isInstance(rhs)) { + testClass = lhsClass; + if (!rhsClass.isInstance(lhs)) { + // rhsClass is a subclass of lhsClass + testClass = rhsClass; + } + } else if (rhsClass.isInstance(lhs)) { + testClass = rhsClass; + if (!lhsClass.isInstance(rhs)) { + // lhsClass is a subclass of rhsClass + testClass = lhsClass; + } + } else { + // The two classes are not related. + return false; + } + final EqualsBuilder equalsBuilder = new EqualsBuilder(); + try { + if (testClass.isArray()) { + equalsBuilder.append(lhs, rhs); + } else { + reflectionAppend(lhs, rhs, testClass, equalsBuilder, testTransients, excludeFields); + while (testClass.getSuperclass() != null && testClass != reflectUpToClass) { + testClass = testClass.getSuperclass(); + reflectionAppend(lhs, rhs, testClass, equalsBuilder, testTransients, excludeFields); + } + } + } catch (final IllegalArgumentException e) { + // In this case, we tried to test a subclass vs. a superclass and + // the subclass has ivars or the ivars are transient and + // we are testing transients. + // If a subclass has ivars that we are trying to test them, we get an + // exception and we know that the objects are not equal. + return false; + } + return equalsBuilder.isEquals(); + } + + /** + *

Appends the fields and values defined by the given object of the + * given Class.

+ * + * @param lhs the left hand object + * @param rhs the right hand object + * @param clazz the class to append details of + * @param builder the builder to append to + * @param useTransients whether to test transient fields + * @param excludeFields array of field names to exclude from testing + */ + private static void reflectionAppend( + final Object lhs, + final Object rhs, + final Class clazz, + final EqualsBuilder builder, + final boolean useTransients, + final String[] excludeFields) { + + if (isRegistered(lhs, rhs)) { + return; + } + + try { + register(lhs, rhs); + final Field[] fields = clazz.getDeclaredFields(); + AccessibleObject.setAccessible(fields, true); + for (int i = 0; i < fields.length && builder.isEquals; i++) { + final Field f = fields[i]; + if (!ArrayUtil.contains(excludeFields, f.getName()) + && (f.getName().indexOf('$') == -1) + && (useTransients || !Modifier.isTransient(f.getModifiers())) + && (!Modifier.isStatic(f.getModifiers()))) { + try { + builder.append(f.get(lhs), f.get(rhs)); + } catch (final IllegalAccessException e) { + //this can't happen. Would get a Security exception instead + //throw a runtime exception in case the impossible happens. + throw new InternalError("Unexpected IllegalAccessException"); + } + } + } + } finally { + unregister(lhs, rhs); + } + } + + //------------------------------------------------------------------------- + + /** + *

Adds the result of {@code super.equals()} to this builder.

+ * + * @param superEquals the result of calling {@code super.equals()} + * @return EqualsBuilder - used to chain calls. + * @since 2.0 + */ + public EqualsBuilder appendSuper(final boolean superEquals) { + if (!isEquals) { + return this; + } + isEquals = superEquals; + return this; + } + + //------------------------------------------------------------------------- + + /** + *

Test if two {@code Object}s are equal using their + * {@code equals} method.

+ * + * @param lhs the left hand object + * @param rhs the right hand object + * @return EqualsBuilder - used to chain calls. + */ + public EqualsBuilder append(final Object lhs, final Object rhs) { + if (!isEquals) { + return this; + } + if (lhs == rhs) { + return this; + } + if (lhs == null || rhs == null) { + return setEquals(false); + } + if (ArrayUtil.isArray(lhs)) { + // 判断数组的equals + return setEquals(ArrayUtil.equals(lhs, rhs)); + } + + // The simple case, not an array, just test the element + return setEquals(lhs.equals(rhs)); + } + + /** + *

+ * Test if two {@code long} s are equal. + *

+ * + * @param lhs the left hand {@code long} + * @param rhs the right hand {@code long} + * @return EqualsBuilder - used to chain calls. + */ + public EqualsBuilder append(final long lhs, final long rhs) { + if (!isEquals) { + return this; + } + isEquals = (lhs == rhs); + return this; + } + + /** + *

Test if two {@code int}s are equal.

+ * + * @param lhs the left hand {@code int} + * @param rhs the right hand {@code int} + * @return EqualsBuilder - used to chain calls. + */ + public EqualsBuilder append(final int lhs, final int rhs) { + if (!isEquals) { + return this; + } + isEquals = (lhs == rhs); + return this; + } + + /** + *

Test if two {@code short}s are equal.

+ * + * @param lhs the left hand {@code short} + * @param rhs the right hand {@code short} + * @return EqualsBuilder - used to chain calls. + */ + public EqualsBuilder append(final short lhs, final short rhs) { + if (!isEquals) { + return this; + } + isEquals = (lhs == rhs); + return this; + } + + /** + *

Test if two {@code char}s are equal.

+ * + * @param lhs the left hand {@code char} + * @param rhs the right hand {@code char} + * @return EqualsBuilder - used to chain calls. + */ + public EqualsBuilder append(final char lhs, final char rhs) { + if (!isEquals) { + return this; + } + isEquals = (lhs == rhs); + return this; + } + + /** + *

Test if two {@code byte}s are equal.

+ * + * @param lhs the left hand {@code byte} + * @param rhs the right hand {@code byte} + * @return EqualsBuilder - used to chain calls. + */ + public EqualsBuilder append(final byte lhs, final byte rhs) { + if (!isEquals) { + return this; + } + isEquals = (lhs == rhs); + return this; + } + + /** + *

Test if two {@code double}s are equal by testing that the + * pattern of bits returned by {@code doubleToLong} are equal.

+ * + *

This handles NaNs, Infinities, and {@code -0.0}.

+ * + *

It is compatible with the hash code generated by + * {@code HashCodeBuilder}.

+ * + * @param lhs the left hand {@code double} + * @param rhs the right hand {@code double} + * @return EqualsBuilder - used to chain calls. + */ + public EqualsBuilder append(final double lhs, final double rhs) { + if (!isEquals) { + return this; + } + return append(Double.doubleToLongBits(lhs), Double.doubleToLongBits(rhs)); + } + + /** + *

Test if two {@code float}s are equal byt testing that the + * pattern of bits returned by doubleToLong are equal.

+ * + *

This handles NaNs, Infinities, and {@code -0.0}.

+ * + *

It is compatible with the hash code generated by + * {@code HashCodeBuilder}.

+ * + * @param lhs the left hand {@code float} + * @param rhs the right hand {@code float} + * @return EqualsBuilder - used to chain calls. + */ + public EqualsBuilder append(final float lhs, final float rhs) { + if (!isEquals) { + return this; + } + return append(Float.floatToIntBits(lhs), Float.floatToIntBits(rhs)); + } + + /** + *

Test if two {@code booleans}s are equal.

+ * + * @param lhs the left hand {@code boolean} + * @param rhs the right hand {@code boolean} + * @return EqualsBuilder - used to chain calls. + */ + public EqualsBuilder append(final boolean lhs, final boolean rhs) { + if (!isEquals) { + return this; + } + isEquals = (lhs == rhs); + return this; + } + + /** + *

Returns {@code true} if the fields that have been checked + * are all equal.

+ * + * @return boolean + */ + public boolean isEquals() { + return this.isEquals; + } + + /** + *

Returns {@code true} if the fields that have been checked + * are all equal.

+ * + * @return {@code true} if all of the fields that have been checked + * are equal, {@code false} otherwise. + * @since 3.0 + */ + @Override + public Boolean build() { + return isEquals(); + } + + /** + * Sets the {@code isEquals} value. + * + * @param isEquals The value to set. + * @return this + */ + protected EqualsBuilder setEquals(boolean isEquals) { + this.isEquals = isEquals; + return this; + } + + /** + * Reset the EqualsBuilder so you can use the same object again + * + * @since 2.5 + */ + public void reset() { + this.isEquals = true; + } +} diff --git a/src/main/java/cn/hutool/core/builder/GenericBuilder.java b/src/main/java/cn/hutool/core/builder/GenericBuilder.java new file mode 100644 index 0000000..817df55 --- /dev/null +++ b/src/main/java/cn/hutool/core/builder/GenericBuilder.java @@ -0,0 +1,235 @@ +package cn.hutool.core.builder; + +import cn.hutool.core.lang.func.Consumer3; +import cn.hutool.core.lang.func.Supplier1; +import cn.hutool.core.lang.func.Supplier2; +import cn.hutool.core.lang.func.Supplier3; +import cn.hutool.core.lang.func.Supplier4; +import cn.hutool.core.lang.func.Supplier5; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import java.util.function.Supplier; + +/** + *

通用Builder

+ * 参考: 一看就会的java8通用Builder + *

使用方法如下:

+ *
+ * Box box = GenericBuilder
+ * 		.of(Box::new)
+ * 		.with(Box::setId, 1024L)
+ * 		.with(Box::setTitle, "Hello World!")
+ * 		.with(Box::setLength, 9)
+ * 		.with(Box::setWidth, 8)
+ * 		.with(Box::setHeight, 7)
+ * 		.build();
+ *
+ * 
+ * + *

我们也可以对已创建的对象进行修改:

+ *
+ * Box boxModified = GenericBuilder
+ * 		.of(() -> box)
+ * 		.with(Box::setTitle, "Hello Friend!")
+ * 		.with(Box::setLength, 3)
+ * 		.with(Box::setWidth, 4)
+ * 		.with(Box::setHeight, 5)
+ * 		.build();
+ * 
+ *

我们还可以对这样调用有参构造,这对于创建一些在有参构造中包含初始化函数的对象是有意义的:

+ *
+ * Box box1 = GenericBuilder
+ * 		.of(Box::new, 2048L, "Hello Partner!", 222, 333, 444)
+ * 		.with(Box::alis)
+ * 		.build();
+ * 
+ *

还可能这样构建Map对象:

+ * {@code + * HashMap colorMap = GenericBuilder + * .of(HashMap::new) + * .with(Map::put, "red", "#FF0000") + * .with(Map::put, "yellow", "#FFFF00") + * .with(Map::put, "blue", "#0000FF") + * .build(); + * } + * + *

注意:本工具类支持调用的构造方法的参数数量不超过5个,一般方法的参数数量不超过2个,更多的参数不利于阅读和维护。

+ * + * @author TomXin + * @since 5.7.21 + */ +public class GenericBuilder implements Builder { + private static final long serialVersionUID = 1L; + + /** + * 实例化器 + */ + private final Supplier instant; + + /** + * 修改器列表 + */ + private final List> modifiers = new ArrayList<>(); + + /** + * 构造 + * + * @param instant 实例化器 + */ + public GenericBuilder(Supplier instant) { + this.instant = instant; + } + + /** + * 通过无参数实例化器创建GenericBuilder + * + * @param instant 实例化器 + * @param 目标类型 + * @return GenericBuilder对象 + */ + public static GenericBuilder of(Supplier instant) { + return new GenericBuilder<>(instant); + } + + /** + * 通过1参数实例化器创建GenericBuilder + * + * @param instant 实例化器 + * @param p1 参数一 + * @param 目标类型 + * @param 参数一类型 + * @return GenericBuilder对象 + */ + public static GenericBuilder of(Supplier1 instant, P1 p1) { + return of(instant.toSupplier(p1)); + } + + /** + * 通过2参数实例化器创建GenericBuilder + * + * @param instant 实例化器 + * @param p1 参数一 + * @param p2 参数二 + * @param 目标类型 + * @param 参数一类型 + * @param 参数二类型 + * @return GenericBuilder对象 + */ + public static GenericBuilder of(Supplier2 instant, P1 p1, P2 p2) { + return of(instant.toSupplier(p1, p2)); + } + + /** + * 通过3参数实例化器创建GenericBuilder + * + * @param instant 实例化器 + * @param p1 参数一 + * @param p2 参数二 + * @param p3 参数三 + * @param 目标类型 + * @param 参数一类型 + * @param 参数二类型 + * @param 参数三类型 + * @return GenericBuilder对象 + */ + public static GenericBuilder of(Supplier3 instant, P1 p1, P2 p2, P3 p3) { + return of(instant.toSupplier(p1, p2, p3)); + } + + /** + * 通过4参数实例化器创建GenericBuilder + * + * @param instant 实例化器 + * @param p1 参数一 + * @param p2 参数二 + * @param p3 参数三 + * @param p4 参数四 + * @param 目标类型 + * @param 参数一类型 + * @param 参数二类型 + * @param 参数三类型 + * @param 参数四类型 + * @return GenericBuilder对象 + */ + public static GenericBuilder of(Supplier4 instant, P1 p1, P2 p2, P3 p3, P4 p4) { + return of(instant.toSupplier(p1, p2, p3, p4)); + } + + /** + * 通过5参数实例化器创建GenericBuilder + * + * @param instant 实例化器 + * @param p1 参数一 + * @param p2 参数二 + * @param p3 参数三 + * @param p4 参数四 + * @param p5 参数五 + * @param 目标类型 + * @param 参数一类型 + * @param 参数二类型 + * @param 参数三类型 + * @param 参数四类型 + * @param 参数五类型 + * @return GenericBuilder对象 + */ + public static GenericBuilder of(Supplier5 instant, P1 p1, P2 p2, P3 p3, P4 p4, P5 p5) { + return of(instant.toSupplier(p1, p2, p3, p4, p5)); + } + + + /** + * 调用无参数方法 + * + * @param consumer 无参数Consumer + * @return GenericBuilder对象 + */ + public GenericBuilder with(Consumer consumer) { + modifiers.add(consumer); + return this; + } + + + /** + * 调用1参数方法 + * + * @param consumer 1参数Consumer + * @param p1 参数一 + * @param 参数一类型 + * @return GenericBuilder对象 + */ + public GenericBuilder with(BiConsumer consumer, P1 p1) { + modifiers.add(instant -> consumer.accept(instant, p1)); + return this; + } + + /** + * 调用2参数方法 + * + * @param consumer 2参数Consumer + * @param p1 参数一 + * @param p2 参数二 + * @param 参数一类型 + * @param 参数二类型 + * @return GenericBuilder对象 + */ + public GenericBuilder with(Consumer3 consumer, P1 p1, P2 p2) { + modifiers.add(instant -> consumer.accept(instant, p1, p2)); + return this; + } + + /** + * 构建 + * + * @return 目标对象 + */ + @Override + public T build() { + T value = instant.get(); + modifiers.forEach(modifier -> modifier.accept(value)); + modifiers.clear(); + return value; + } +} diff --git a/src/main/java/cn/hutool/core/builder/HashCodeBuilder.java b/src/main/java/cn/hutool/core/builder/HashCodeBuilder.java new file mode 100644 index 0000000..873d56a --- /dev/null +++ b/src/main/java/cn/hutool/core/builder/HashCodeBuilder.java @@ -0,0 +1,958 @@ +package cn.hutool.core.builder; + +import java.lang.reflect.AccessibleObject; +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; + +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.ArrayUtil; + +/** + *

+ * Assists in implementing {@link Object#hashCode()} methods. + *

+ * + *

+ * This class enables a good hashCode method to be built for any class. It follows the rules laid out in + * the book Effective Java by Joshua Bloch. Writing a + * good hashCode method is actually quite difficult. This class aims to simplify the process. + *

+ * + *

+ * The following is the approach taken. When appending a data field, the current total is multiplied by the + * multiplier then a relevant value + * for that data type is added. For example, if the current hashCode is 17, and the multiplier is 37, then + * appending the integer 45 will create a hashcode of 674, namely 17 * 37 + 45. + *

+ * + *

+ * All relevant fields from the object should be included in the hashCode method. Derived fields may be + * excluded. In general, any field used in the equals method must be used in the hashCode + * method. + *

+ * + *

+ * To use this class write code as follows: + *

+ * + *
+ * public class Person {
+ *   String name;
+ *   int age;
+ *   boolean smoker;
+ *   ...
+ *
+ *   public int hashCode() {
+ *     // you pick a hard-coded, randomly chosen, non-zero, odd number
+ *     // ideally different for each class
+ *     return new HashCodeBuilder(17, 37).
+ *       append(name).
+ *       append(age).
+ *       append(smoker).
+ *       toHashCode();
+ *   }
+ * }
+ * 
+ * + *

+ * If required, the superclass hashCode() can be added using {@link #appendSuper}. + *

+ * + *

+ * Alternatively, there is a method that uses reflection to determine the fields to test. Because these fields are + * usually private, the method, reflectionHashCode, uses AccessibleObject.setAccessible + * to change the visibility of the fields. This will fail under a security manager, unless the appropriate permissions + * are set up correctly. It is also slower than testing explicitly. + *

+ * + *

+ * A typical invocation for this method would look like: + *

+ * + *
+ * public int hashCode() {
+ *   return HashCodeBuilder.reflectionHashCode(this);
+ * }
+ * 
+ * + * TODO 待整理 + * 来自于Apache-Commons-Lang3 + * @author looly,Apache-Commons + * @since 4.2.2 + */ +public class HashCodeBuilder implements Builder { + private static final long serialVersionUID = 1L; + + /** + * The default initial value to use in reflection hash code building. + */ + private static final int DEFAULT_INITIAL_VALUE = 17; + + /** + * The default multipler value to use in reflection hash code building. + */ + private static final int DEFAULT_MULTIPLIER_VALUE = 37; + + /** + *

+ * A registry of objects used by reflection methods to detect cyclical object references and avoid infinite loops. + *

+ * + * @since 2.3 + */ + private static final ThreadLocal> REGISTRY = new ThreadLocal<>(); + + /* + * NOTE: we cannot store the actual objects in a HashSet, as that would use the very hashCode() + * we are in the process of calculating. + * + * So we generate a one-to-one mapping from the original object to a new object. + * + * Now HashSet uses equals() to determine if two elements with the same hashcode really + * are equal, so we also need to ensure that the replacement objects are only equal + * if the original objects are identical. + * + * The original implementation (2.4 and before) used the System.indentityHashCode() + * method - however this is not guaranteed to generate unique ids (e.g. LANG-459) + * + * We now use the IDKey helper class (adapted from org.apache.axis.utils.IDKey) + * to disambiguate the duplicate ids. + */ + + /** + *

+ * Returns the registry of objects being traversed by the reflection methods in the current thread. + *

+ * + * @return Set the registry of objects being traversed + * @since 2.3 + */ + private static Set getRegistry() { + return REGISTRY.get(); + } + + /** + *

+ * Returns true if the registry contains the given object. Used by the reflection methods to avoid + * infinite loops. + *

+ * + * @param value + * The object to lookup in the registry. + * @return boolean true if the registry contains the given object. + * @since 2.3 + */ + private static boolean isRegistered(final Object value) { + final Set registry = getRegistry(); + return registry != null && registry.contains(new IDKey(value)); + } + + /** + *

+ * Appends the fields and values defined by the given object of the given Class. + *

+ * + * @param object + * the object to append details of + * @param clazz + * the class to append details of + * @param builder + * the builder to append to + * @param useTransients + * whether to use transient fields + * @param excludeFields + * Collection of String field names to exclude from use in calculation of hash code + */ + private static void reflectionAppend(final Object object, final Class clazz, final HashCodeBuilder builder, final boolean useTransients, + final String[] excludeFields) { + if (isRegistered(object)) { + return; + } + try { + register(object); + final Field[] fields = clazz.getDeclaredFields(); + AccessibleObject.setAccessible(fields, true); + for (final Field field : fields) { + if (!ArrayUtil.contains(excludeFields, field.getName()) + && (field.getName().indexOf('$') == -1) + && (useTransients || !Modifier.isTransient(field.getModifiers())) + && (!Modifier.isStatic(field.getModifiers()))) { + try { + final Object fieldValue = field.get(object); + builder.append(fieldValue); + } catch (final IllegalAccessException e) { + // this can't happen. Would get a Security exception instead + // throw a runtime exception in case the impossible happens. + throw new InternalError("Unexpected IllegalAccessException"); + } + } + } + } finally { + unregister(object); + } + } + + /** + *

+ * Uses reflection to build a valid hash code from the fields of {@code object}. + *

+ * + *

+ * It uses AccessibleObject.setAccessible to gain access to private fields. This means that it will + * throw a security exception if run under a security manager, if the permissions are not set up correctly. It is + * also not as efficient as testing explicitly. + *

+ * + *

+ * Transient members will be not be used, as they are likely derived fields, and not part of the value of the + * Object. + *

+ * + *

+ * Static fields will not be tested. Superclass fields will be included. + *

+ * + *

+ * Two randomly chosen, non-zero, odd numbers must be passed in. Ideally these should be different for each class, + * however this is not vital. Prime numbers are preferred, especially for the multiplier. + *

+ * + * @param initialNonZeroOddNumber + * a non-zero, odd number used as the initial value. This will be the returned + * value if no fields are found to include in the hash code + * @param multiplierNonZeroOddNumber + * a non-zero, odd number used as the multiplier + * @param object + * the Object to create a hashCode for + * @return int hash code + * @throws IllegalArgumentException + * if the Object is null + * @throws IllegalArgumentException + * if the number is zero or even + */ + public static int reflectionHashCode(final int initialNonZeroOddNumber, final int multiplierNonZeroOddNumber, final Object object) { + return reflectionHashCode(initialNonZeroOddNumber, multiplierNonZeroOddNumber, object, false, null); + } + + /** + *

+ * Uses reflection to build a valid hash code from the fields of {@code object}. + *

+ * + *

+ * It uses AccessibleObject.setAccessible to gain access to private fields. This means that it will + * throw a security exception if run under a security manager, if the permissions are not set up correctly. It is + * also not as efficient as testing explicitly. + *

+ * + *

+ * If the TestTransients parameter is set to true, transient members will be tested, otherwise they + * are ignored, as they are likely derived fields, and not part of the value of the Object. + *

+ * + *

+ * Static fields will not be tested. Superclass fields will be included. + *

+ * + *

+ * Two randomly chosen, non-zero, odd numbers must be passed in. Ideally these should be different for each class, + * however this is not vital. Prime numbers are preferred, especially for the multiplier. + *

+ * + * @param initialNonZeroOddNumber + * a non-zero, odd number used as the initial value. This will be the returned + * value if no fields are found to include in the hash code + * @param multiplierNonZeroOddNumber + * a non-zero, odd number used as the multiplier + * @param object + * the Object to create a hashCode for + * @param testTransients + * whether to include transient fields + * @return int hash code + * @throws IllegalArgumentException + * if the Object is null + * @throws IllegalArgumentException + * if the number is zero or even + */ + public static int reflectionHashCode(final int initialNonZeroOddNumber, final int multiplierNonZeroOddNumber, final Object object, + final boolean testTransients) { + return reflectionHashCode(initialNonZeroOddNumber, multiplierNonZeroOddNumber, object, testTransients, null); + } + + /** + *

+ * Uses reflection to build a valid hash code from the fields of {@code object}. + *

+ * + *

+ * It uses AccessibleObject.setAccessible to gain access to private fields. This means that it will + * throw a security exception if run under a security manager, if the permissions are not set up correctly. It is + * also not as efficient as testing explicitly. + *

+ * + *

+ * If the TestTransients parameter is set to true, transient members will be tested, otherwise they + * are ignored, as they are likely derived fields, and not part of the value of the Object. + *

+ * + *

+ * Static fields will not be included. Superclass fields will be included up to and including the specified + * superclass. A null superclass is treated as java.lang.Object. + *

+ * + *

+ * Two randomly chosen, non-zero, odd numbers must be passed in. Ideally these should be different for each class, + * however this is not vital. Prime numbers are preferred, especially for the multiplier. + *

+ * + * @param + * the type of the object involved + * @param initialNonZeroOddNumber + * a non-zero, odd number used as the initial value. This will be the returned + * value if no fields are found to include in the hash code + * @param multiplierNonZeroOddNumber + * a non-zero, odd number used as the multiplier + * @param object + * the Object to create a hashCode for + * @param testTransients + * whether to include transient fields + * @param reflectUpToClass + * the superclass to reflect up to (inclusive), may be null + * @param excludeFields + * array of field names to exclude from use in calculation of hash code + * @return int hash code + * @throws IllegalArgumentException + * if the Object is null + * @throws IllegalArgumentException + * if the number is zero or even + * @since 2.0 + */ + public static int reflectionHashCode(final int initialNonZeroOddNumber, final int multiplierNonZeroOddNumber, final T object, + final boolean testTransients, final Class reflectUpToClass, final String... excludeFields) { + + if (object == null) { + throw new IllegalArgumentException("The object to build a hash code for must not be null"); + } + final HashCodeBuilder builder = new HashCodeBuilder(initialNonZeroOddNumber, multiplierNonZeroOddNumber); + Class clazz = object.getClass(); + reflectionAppend(object, clazz, builder, testTransients, excludeFields); + while (clazz.getSuperclass() != null && clazz != reflectUpToClass) { + clazz = clazz.getSuperclass(); + reflectionAppend(object, clazz, builder, testTransients, excludeFields); + } + return builder.toHashCode(); + } + + /** + *

+ * Uses reflection to build a valid hash code from the fields of {@code object}. + *

+ * + *

+ * This constructor uses two hard coded choices for the constants needed to build a hash code. + *

+ * + *

+ * It uses AccessibleObject.setAccessible to gain access to private fields. This means that it will + * throw a security exception if run under a security manager, if the permissions are not set up correctly. It is + * also not as efficient as testing explicitly. + *

+ * + *

+ * If the TestTransients parameter is set to true, transient members will be tested, otherwise they + * are ignored, as they are likely derived fields, and not part of the value of the Object. + *

+ * + *

+ * Static fields will not be tested. Superclass fields will be included. If no fields are found to include + * in the hash code, the result of this method will be constant. + *

+ * + * @param object + * the Object to create a hashCode for + * @param testTransients + * whether to include transient fields + * @return int hash code + * @throws IllegalArgumentException + * if the object is null + */ + public static int reflectionHashCode(final Object object, final boolean testTransients) { + return reflectionHashCode(DEFAULT_INITIAL_VALUE, DEFAULT_MULTIPLIER_VALUE, object, + testTransients, null); + } + + /** + *

+ * Uses reflection to build a valid hash code from the fields of {@code object}. + *

+ * + *

+ * This constructor uses two hard coded choices for the constants needed to build a hash code. + *

+ * + *

+ * It uses AccessibleObject.setAccessible to gain access to private fields. This means that it will + * throw a security exception if run under a security manager, if the permissions are not set up correctly. It is + * also not as efficient as testing explicitly. + *

+ * + *

+ * Transient members will be not be used, as they are likely derived fields, and not part of the value of the + * Object. + *

+ * + *

+ * Static fields will not be tested. Superclass fields will be included. If no fields are found to include + * in the hash code, the result of this method will be constant. + *

+ * + * @param object + * the Object to create a hashCode for + * @param excludeFields + * Collection of String field names to exclude from use in calculation of hash code + * @return int hash code + * @throws IllegalArgumentException + * if the object is null + */ + public static int reflectionHashCode(final Object object, final Collection excludeFields) { + return reflectionHashCode(object, ArrayUtil.toArray(excludeFields, String.class)); + } + + // ------------------------------------------------------------------------- + + /** + *

+ * Uses reflection to build a valid hash code from the fields of {@code object}. + *

+ * + *

+ * This constructor uses two hard coded choices for the constants needed to build a hash code. + *

+ * + *

+ * It uses AccessibleObject.setAccessible to gain access to private fields. This means that it will + * throw a security exception if run under a security manager, if the permissions are not set up correctly. It is + * also not as efficient as testing explicitly. + *

+ * + *

+ * Transient members will be not be used, as they are likely derived fields, and not part of the value of the + * Object. + *

+ * + *

+ * Static fields will not be tested. Superclass fields will be included. If no fields are found to include + * in the hash code, the result of this method will be constant. + *

+ * + * @param object + * the Object to create a hashCode for + * @param excludeFields + * array of field names to exclude from use in calculation of hash code + * @return int hash code + * @throws IllegalArgumentException + * if the object is null + */ + public static int reflectionHashCode(final Object object, final String... excludeFields) { + return reflectionHashCode(DEFAULT_INITIAL_VALUE, DEFAULT_MULTIPLIER_VALUE, object, false, + null, excludeFields); + } + + /** + *

+ * Registers the given object. Used by the reflection methods to avoid infinite loops. + *

+ * + * @param value + * The object to register. + */ + static void register(final Object value) { + synchronized (HashCodeBuilder.class) { + if (getRegistry() == null) { + REGISTRY.set(new HashSet()); + } + } + getRegistry().add(new IDKey(value)); + } + + /** + *

+ * Unregisters the given object. + *

+ * + *

+ * Used by the reflection methods to avoid infinite loops. + * + * @param value + * The object to unregister. + * @since 2.3 + */ + static void unregister(final Object value) { + Set registry = getRegistry(); + if (registry != null) { + registry.remove(new IDKey(value)); + synchronized (HashCodeBuilder.class) { + //read again + registry = getRegistry(); + if (registry != null && registry.isEmpty()) { + REGISTRY.remove(); + } + } + } + } + + /** + * Constant to use in building the hashCode. + */ + private final int iConstant; + + /** + * Running total of the hashCode. + */ + private int iTotal; + + /** + *

+ * Uses two hard coded choices for the constants needed to build a hashCode. + *

+ */ + public HashCodeBuilder() { + iConstant = 37; + iTotal = 17; + } + + /** + *

+ * Two randomly chosen, odd numbers must be passed in. Ideally these should be different for each class, + * however this is not vital. + *

+ * + *

+ * Prime numbers are preferred, especially for the multiplier. + *

+ * + * @param initialOddNumber + * an odd number used as the initial value + * @param multiplierOddNumber + * an odd number used as the multiplier + * @throws IllegalArgumentException + * if the number is even + */ + public HashCodeBuilder(final int initialOddNumber, final int multiplierOddNumber) { + Assert.isTrue(initialOddNumber % 2 != 0, "HashCodeBuilder requires an odd initial value"); + Assert.isTrue(multiplierOddNumber % 2 != 0, "HashCodeBuilder requires an odd multiplier"); + iConstant = multiplierOddNumber; + iTotal = initialOddNumber; + } + + /** + *

+ * Append a hashCode for a boolean. + *

+ *

+ * This adds 1 when true, and 0 when false to the hashCode. + *

+ *

+ * This is in contrast to the standard java.lang.Boolean.hashCode handling, which computes + * a hashCode value of 1231 for java.lang.Boolean instances + * that represent true or 1237 for java.lang.Boolean instances + * that represent false. + *

+ *

+ * This is in accordance with the Effective Java design. + *

+ * + * @param value + * the boolean to add to the hashCode + * @return this + */ + public HashCodeBuilder append(final boolean value) { + iTotal = iTotal * iConstant + (value ? 0 : 1); + return this; + } + + /** + *

+ * Append a hashCode for a boolean array. + *

+ * + * @param array + * the array to add to the hashCode + * @return this + */ + public HashCodeBuilder append(final boolean[] array) { + if (array == null) { + iTotal = iTotal * iConstant; + } else { + for (final boolean element : array) { + append(element); + } + } + return this; + } + + // ------------------------------------------------------------------------- + + /** + *

+ * Append a hashCode for a byte. + *

+ * + * @param value + * the byte to add to the hashCode + * @return this + */ + public HashCodeBuilder append(final byte value) { + iTotal = iTotal * iConstant + value; + return this; + } + + // ------------------------------------------------------------------------- + + /** + *

+ * Append a hashCode for a byte array. + *

+ * + * @param array + * the array to add to the hashCode + * @return this + */ + public HashCodeBuilder append(final byte[] array) { + if (array == null) { + iTotal = iTotal * iConstant; + } else { + for (final byte element : array) { + append(element); + } + } + return this; + } + + /** + *

+ * Append a hashCode for a char. + *

+ * + * @param value + * the char to add to the hashCode + * @return this + */ + public HashCodeBuilder append(final char value) { + iTotal = iTotal * iConstant + value; + return this; + } + + /** + *

+ * Append a hashCode for a char array. + *

+ * + * @param array + * the array to add to the hashCode + * @return this + */ + public HashCodeBuilder append(final char[] array) { + if (array == null) { + iTotal = iTotal * iConstant; + } else { + for (final char element : array) { + append(element); + } + } + return this; + } + + /** + *

+ * Append a hashCode for a double. + *

+ * + * @param value + * the double to add to the hashCode + * @return this + */ + public HashCodeBuilder append(final double value) { + return append(Double.doubleToLongBits(value)); + } + + /** + *

+ * Append a hashCode for a double array. + *

+ * + * @param array + * the array to add to the hashCode + * @return this + */ + public HashCodeBuilder append(final double[] array) { + if (array == null) { + iTotal = iTotal * iConstant; + } else { + for (final double element : array) { + append(element); + } + } + return this; + } + + /** + *

+ * Append a hashCode for a float. + *

+ * + * @param value + * the float to add to the hashCode + * @return this + */ + public HashCodeBuilder append(final float value) { + iTotal = iTotal * iConstant + Float.floatToIntBits(value); + return this; + } + + /** + *

+ * Append a hashCode for a float array. + *

+ * + * @param array + * the array to add to the hashCode + * @return this + */ + public HashCodeBuilder append(final float[] array) { + if (array == null) { + iTotal = iTotal * iConstant; + } else { + for (final float element : array) { + append(element); + } + } + return this; + } + + /** + *

+ * Append a hashCode for an int. + *

+ * + * @param value + * the int to add to the hashCode + * @return this + */ + public HashCodeBuilder append(final int value) { + iTotal = iTotal * iConstant + value; + return this; + } + + /** + *

+ * Append a hashCode for an int array. + *

+ * + * @param array + * the array to add to the hashCode + * @return this + */ + public HashCodeBuilder append(final int[] array) { + if (array == null) { + iTotal = iTotal * iConstant; + } else { + for (final int element : array) { + append(element); + } + } + return this; + } + + /** + *

+ * Append a hashCode for a long. + *

+ * + * @param value + * the long to add to the hashCode + * @return this + */ + // NOTE: This method uses >> and not >>> as Effective Java and + // Long.hashCode do. Ideally we should switch to >>> at + // some stage. There are backwards compat issues, so + // that will have to wait for the time being. cf LANG-342. + public HashCodeBuilder append(final long value) { + iTotal = iTotal * iConstant + ((int) (value ^ (value >> 32))); + return this; + } + + /** + *

+ * Append a hashCode for a long array. + *

+ * + * @param array + * the array to add to the hashCode + * @return this + */ + public HashCodeBuilder append(final long[] array) { + if (array == null) { + iTotal = iTotal * iConstant; + } else { + for (final long element : array) { + append(element); + } + } + return this; + } + + /** + *

+ * Append a hashCode for an Object. + *

+ * + * @param object + * the Object to add to the hashCode + * @return this + */ + public HashCodeBuilder append(final Object object) { + if (object == null) { + iTotal = iTotal * iConstant; + + } else { + if(object.getClass().isArray()) { + // 'Switch' on type of array, to dispatch to the correct handler + // This handles multi dimensional arrays + if (object instanceof long[]) { + append((long[]) object); + } else if (object instanceof int[]) { + append((int[]) object); + } else if (object instanceof short[]) { + append((short[]) object); + } else if (object instanceof char[]) { + append((char[]) object); + } else if (object instanceof byte[]) { + append((byte[]) object); + } else if (object instanceof double[]) { + append((double[]) object); + } else if (object instanceof float[]) { + append((float[]) object); + } else if (object instanceof boolean[]) { + append((boolean[]) object); + } else { + // Not an array of primitives + append((Object[]) object); + } + } else { + iTotal = iTotal * iConstant + object.hashCode(); + } + } + return this; + } + + /** + *

+ * Append a hashCode for an Object array. + *

+ * + * @param array + * the array to add to the hashCode + * @return this + */ + public HashCodeBuilder append(final Object[] array) { + if (array == null) { + iTotal = iTotal * iConstant; + } else { + for (final Object element : array) { + append(element); + } + } + return this; + } + + /** + *

+ * Append a hashCode for a short. + *

+ * + * @param value + * the short to add to the hashCode + * @return this + */ + public HashCodeBuilder append(final short value) { + iTotal = iTotal * iConstant + value; + return this; + } + + /** + *

+ * Append a hashCode for a short array. + *

+ * + * @param array + * the array to add to the hashCode + * @return this + */ + public HashCodeBuilder append(final short[] array) { + if (array == null) { + iTotal = iTotal * iConstant; + } else { + for (final short element : array) { + append(element); + } + } + return this; + } + + /** + *

+ * Adds the result of super.hashCode() to this builder. + *

+ * + * @param superHashCode + * the result of calling super.hashCode() + * @return this HashCodeBuilder, used to chain calls. + * @since 2.0 + */ + public HashCodeBuilder appendSuper(final int superHashCode) { + iTotal = iTotal * iConstant + superHashCode; + return this; + } + + /** + *

+ * Return the computed hashCode. + *

+ * + * @return hashCode based on the fields appended + */ + public int toHashCode() { + return iTotal; + } + + /** + * Returns the computed hashCode. + * + * @return hashCode based on the fields appended + * + * @since 3.0 + */ + @Override + public Integer build() { + return toHashCode(); + } + + /** + *

+ * The computed hashCode from toHashCode() is returned due to the likelihood + * of bugs in mis-calling toHashCode() and the unlikeliness of it mattering what the hashCode for + * HashCodeBuilder itself is.

+ * + * @return hashCode based on the fields appended + * @since 2.5 + */ + @Override + public int hashCode() { + return toHashCode(); + } + +} diff --git a/src/main/java/cn/hutool/core/builder/IDKey.java b/src/main/java/cn/hutool/core/builder/IDKey.java new file mode 100644 index 0000000..e49ecd1 --- /dev/null +++ b/src/main/java/cn/hutool/core/builder/IDKey.java @@ -0,0 +1,61 @@ +package cn.hutool.core.builder; + +import java.io.Serializable; + +/** + * 包装唯一键(System.identityHashCode())使对象只有和自己 equals + * + * 此对象用于消除小概率下System.identityHashCode()产生的ID重复问题。 + * + * 来自于Apache-Commons-Lang3 + * @author looly,Apache-Commons + * @since 4.2.2 + */ +final class IDKey implements Serializable{ + private static final long serialVersionUID = 1L; + + private final Object value; + private final int id; + + /** + * 构造 + * + * @param obj 计算唯一ID的对象 + */ + public IDKey(final Object obj) { + id = System.identityHashCode(obj); + // There have been some cases (LANG-459) that return the + // same identity hash code for different objects. So + // the value is also added to disambiguate these cases. + value = obj; + } + + /** + * returns hashcode - i.e. the system identity hashcode. + * + * @return the hashcode + */ + @Override + public int hashCode() { + return id; + } + + /** + * checks if instances are equal + * + * @param other The other object to compare to + * @return if the instances are for the same object + */ + @Override + public boolean equals(final Object other) { + if (!(other instanceof IDKey)) { + return false; + } + final IDKey idKey = (IDKey) other; + if (id != idKey.id) { + return false; + } + // Note that identity equals is used. + return value == idKey.value; + } +} diff --git a/src/main/java/cn/hutool/core/builder/package-info.java b/src/main/java/cn/hutool/core/builder/package-info.java new file mode 100644 index 0000000..800a028 --- /dev/null +++ b/src/main/java/cn/hutool/core/builder/package-info.java @@ -0,0 +1,8 @@ +/** + * 建造者工具
+ * 用于建造特定对象或结果 + * + * @author looly + * + */ +package cn.hutool.core.builder; \ No newline at end of file diff --git a/src/main/java/cn/hutool/core/clone/CloneRuntimeException.java b/src/main/java/cn/hutool/core/clone/CloneRuntimeException.java new file mode 100644 index 0000000..7f70e28 --- /dev/null +++ b/src/main/java/cn/hutool/core/clone/CloneRuntimeException.java @@ -0,0 +1,32 @@ +package cn.hutool.core.clone; + +import cn.hutool.core.exceptions.ExceptionUtil; +import cn.hutool.core.util.StrUtil; + +/** + * 克隆异常 + * @author xiaoleilu + */ +public class CloneRuntimeException extends RuntimeException{ + private static final long serialVersionUID = 6774837422188798989L; + + public CloneRuntimeException(Throwable e) { + super(ExceptionUtil.getMessage(e), e); + } + + public CloneRuntimeException(String message) { + super(message); + } + + public CloneRuntimeException(String messageTemplate, Object... params) { + super(StrUtil.format(messageTemplate, params)); + } + + public CloneRuntimeException(String message, Throwable throwable) { + super(message, throwable); + } + + public CloneRuntimeException(Throwable throwable, String messageTemplate, Object... params) { + super(StrUtil.format(messageTemplate, params), throwable); + } +} diff --git a/src/main/java/cn/hutool/core/clone/CloneSupport.java b/src/main/java/cn/hutool/core/clone/CloneSupport.java new file mode 100644 index 0000000..3ea2fce --- /dev/null +++ b/src/main/java/cn/hutool/core/clone/CloneSupport.java @@ -0,0 +1,21 @@ +package cn.hutool.core.clone; + +/** + * 克隆支持类,提供默认的克隆方法 + * @author Looly + * + * @param 继承类的类型 + */ +public class CloneSupport implements Cloneable{ + + @SuppressWarnings("unchecked") + @Override + public T clone() { + try { + return (T) super.clone(); + } catch (CloneNotSupportedException e) { + throw new CloneRuntimeException(e); + } + } + +} diff --git a/src/main/java/cn/hutool/core/clone/Cloneable.java b/src/main/java/cn/hutool/core/clone/Cloneable.java new file mode 100644 index 0000000..5fdbb1b --- /dev/null +++ b/src/main/java/cn/hutool/core/clone/Cloneable.java @@ -0,0 +1,16 @@ +package cn.hutool.core.clone; + +/** + * 克隆支持接口 + * @author Looly + * + * @param 实现克隆接口的类型 + */ +public interface Cloneable extends java.lang.Cloneable{ + + /** + * 克隆当前对象,浅复制 + * @return 克隆后的对象 + */ + T clone(); +} diff --git a/src/main/java/cn/hutool/core/clone/DefaultCloneable.java b/src/main/java/cn/hutool/core/clone/DefaultCloneable.java new file mode 100644 index 0000000..8c74f57 --- /dev/null +++ b/src/main/java/cn/hutool/core/clone/DefaultCloneable.java @@ -0,0 +1,28 @@ +package cn.hutool.core.clone; + + +import cn.hutool.core.util.ReflectUtil; + +/** + * 克隆默认实现接口,用于实现返回指定泛型类型的克隆方法 + * + * @param 泛型类型 + * @since 5.7.17 + */ +public interface DefaultCloneable extends java.lang.Cloneable { + + /** + * 浅拷贝,提供默认的泛型返回值的clone方法。 + * + * @return obj + */ + default T clone0() { + try { + return ReflectUtil.invoke(this, "clone"); + } catch (Exception e) { + throw new CloneRuntimeException(e); + } + } +} + + diff --git a/src/main/java/cn/hutool/core/clone/package-info.java b/src/main/java/cn/hutool/core/clone/package-info.java new file mode 100644 index 0000000..a4530d9 --- /dev/null +++ b/src/main/java/cn/hutool/core/clone/package-info.java @@ -0,0 +1,7 @@ +/** + * 克隆封装 + * + * @author looly + * + */ +package cn.hutool.core.clone; \ No newline at end of file diff --git a/src/main/java/cn/hutool/core/codec/BCD.java b/src/main/java/cn/hutool/core/codec/BCD.java new file mode 100644 index 0000000..0bd558a --- /dev/null +++ b/src/main/java/cn/hutool/core/codec/BCD.java @@ -0,0 +1,129 @@ +package cn.hutool.core.codec; + +import cn.hutool.core.lang.Assert; + +/** + * BCD码(Binary-Coded Decimal)亦称二进码十进数或二-十进制代码
+ * BCD码这种编码形式利用了四个位元来储存一个十进制的数码,使二进制和十进制之间的转换得以快捷的进行
+ * see http://cuisuqiang.iteye.com/blog/1429956 + * @author Looly + * + * @deprecated 由于对于ASCII的编码解码有缺陷,且这种BCD实现并不规范,因此会在6.0.0中移除 + */ +@Deprecated +public class BCD { + + /** + * 字符串转BCD码 + * @param asc ASCII字符串 + * @return BCD + */ + public static byte[] strToBcd(String asc) { + Assert.notNull(asc, "ASCII must not be null!"); + int len = asc.length(); + int mod = len % 2; + if (mod != 0) { + asc = "0" + asc; + len = asc.length(); + } + byte[] abt; + if (len >= 2) { + len >>= 1; + } + byte[] bbt; + bbt = new byte[len]; + abt = asc.getBytes(); + int j; + int k; + for (int p = 0; p < asc.length() / 2; p++) { + if ((abt[2 * p] >= '0') && (abt[2 * p] <= '9')) { + j = abt[2 * p] - '0'; + } else if ((abt[2 * p] >= 'a') && (abt[2 * p] <= 'z')) { + j = abt[2 * p] - 'a' + 0x0a; + } else { + j = abt[2 * p] - 'A' + 0x0a; + } + if ((abt[2 * p + 1] >= '0') && (abt[2 * p + 1] <= '9')) { + k = abt[2 * p + 1] - '0'; + } else if ((abt[2 * p + 1] >= 'a') && (abt[2 * p + 1] <= 'z')) { + k = abt[2 * p + 1] - 'a' + 0x0a; + } else { + k = abt[2 * p + 1] - 'A' + 0x0a; + } + int a = (j << 4) + k; + byte b = (byte) a; + bbt[p] = b; + } + return bbt; + } + + /** + * ASCII转BCD + * @param ascii ASCII byte数组 + * @return BCD + */ + public static byte[] ascToBcd(byte[] ascii) { + Assert.notNull(ascii, "Ascii must be not null!"); + return ascToBcd(ascii, ascii.length); + } + + /** + * ASCII转BCD + * @param ascii ASCII byte数组 + * @param ascLength 长度 + * @return BCD + */ + public static byte[] ascToBcd(byte[] ascii, int ascLength) { + Assert.notNull(ascii, "Ascii must be not null!"); + byte[] bcd = new byte[ascLength / 2]; + int j = 0; + for (int i = 0; i < (ascLength + 1) / 2; i++) { + bcd[i] = ascToBcd(ascii[j++]); + bcd[i] = (byte) (((j >= ascLength) ? 0x00 : ascToBcd(ascii[j++])) + (bcd[i] << 4)); + } + return bcd; + } + + /** + * BCD转ASCII字符串 + * @param bytes BCD byte数组 + * @return ASCII字符串 + */ + public static String bcdToStr(byte[] bytes) { + Assert.notNull(bytes, "Bcd bytes must be not null!"); + char[] temp = new char[bytes.length * 2]; + char val; + + for (int i = 0; i < bytes.length; i++) { + val = (char) (((bytes[i] & 0xf0) >> 4) & 0x0f); + temp[i * 2] = (char) (val > 9 ? val + 'A' - 10 : val + '0'); + + val = (char) (bytes[i] & 0x0f); + temp[i * 2 + 1] = (char) (val > 9 ? val + 'A' - 10 : val + '0'); + } + return new String(temp); + } + + + //----------------------------------------------------------------- Private method start + /** + * 转换单个byte为BCD + * @param asc ACSII + * @return BCD + */ + private static byte ascToBcd(byte asc) { + byte bcd; + + if ((asc >= '0') && (asc <= '9')) { + bcd = (byte) (asc - '0'); + }else if ((asc >= 'A') && (asc <= 'F')) { + bcd = (byte) (asc - 'A' + 10); + }else if ((asc >= 'a') && (asc <= 'f')) { + bcd = (byte) (asc - 'a' + 10); + }else { + bcd = (byte) (asc - 48); + } + return bcd; + } + //----------------------------------------------------------------- Private method end +} diff --git a/src/main/java/cn/hutool/core/codec/Base16Codec.java b/src/main/java/cn/hutool/core/codec/Base16Codec.java new file mode 100644 index 0000000..14499d1 --- /dev/null +++ b/src/main/java/cn/hutool/core/codec/Base16Codec.java @@ -0,0 +1,118 @@ +package cn.hutool.core.codec; + +import cn.hutool.core.exceptions.UtilException; +import cn.hutool.core.util.StrUtil; + +/** + * Base16(Hex)编码解码器
+ * 十六进制(简写为hex或下标16)在数学中是一种逢16进1的进位制,一般用数字0到9和字母A到F表示(其中:A~F即10~15)。
+ * 例如十进制数57,在二进制写作111001,在16进制写作39。 + * + * @author looly + * @since 5.7.23 + */ +public class Base16Codec implements Encoder, Decoder { + + public static final Base16Codec CODEC_LOWER = new Base16Codec(true); + public static final Base16Codec CODEC_UPPER = new Base16Codec(false); + + private final char[] alphabets; + + /** + * 构造 + * + * @param lowerCase 是否小写 + */ + public Base16Codec(boolean lowerCase) { + this.alphabets = (lowerCase ? "0123456789abcdef" : "0123456789ABCDEF").toCharArray(); + } + + @Override + public char[] encode(byte[] data) { + final int len = data.length; + final char[] out = new char[len << 1];//len*2 + // two characters from the hex value. + for (int i = 0, j = 0; i < len; i++) { + out[j++] = alphabets[(0xF0 & data[i]) >>> 4];// 高位 + out[j++] = alphabets[0x0F & data[i]];// 低位 + } + return out; + } + + @Override + public byte[] decode(CharSequence encoded) { + if (StrUtil.isEmpty(encoded)) { + return null; + } + + encoded = StrUtil.cleanBlank(encoded); + int len = encoded.length(); + + if ((len & 0x01) != 0) { + // 如果提供的数据是奇数长度,则前面补0凑偶数 + encoded = "0" + encoded; + len = encoded.length(); + } + + final byte[] out = new byte[len >> 1]; + + // two characters form the hex value. + for (int i = 0, j = 0; j < len; i++) { + int f = toDigit(encoded.charAt(j), j) << 4; + j++; + f = f | toDigit(encoded.charAt(j), j); + j++; + out[i] = (byte) (f & 0xFF); + } + + return out; + } + + /** + * 将指定char值转换为Unicode字符串形式,常用于特殊字符(例如汉字)转Unicode形式
+ * 转换的字符串如果u后不足4位,则前面用0填充,例如: + * + *
+	 * '你' =》'\u4f60'
+	 * 
+ * + * @param ch char值 + * @return Unicode表现形式 + */ + public String toUnicodeHex(char ch) { + return "\\u" +// + alphabets[(ch >> 12) & 15] +// + alphabets[(ch >> 8) & 15] +// + alphabets[(ch >> 4) & 15] +// + alphabets[(ch) & 15]; + } + + /** + * 将byte值转为16进制并添加到{@link StringBuilder}中 + * + * @param builder {@link StringBuilder} + * @param b byte + */ + public void appendHex(StringBuilder builder, byte b) { + int high = (b & 0xf0) >>> 4;//高位 + int low = b & 0x0f;//低位 + builder.append(alphabets[high]); + builder.append(alphabets[low]); + } + + /** + * 将十六进制字符转换成一个整数 + * + * @param ch 十六进制char + * @param index 十六进制字符在字符数组中的位置 + * @return 一个整数 + * @throws UtilException 当ch不是一个合法的十六进制字符时,抛出运行时异常 + */ + private static int toDigit(char ch, int index) { + int digit = Character.digit(ch, 16); + if (digit < 0) { + throw new UtilException("Illegal hexadecimal character {} at index {}", ch, index); + } + return digit; + } +} diff --git a/src/main/java/cn/hutool/core/codec/Base32.java b/src/main/java/cn/hutool/core/codec/Base32.java new file mode 100644 index 0000000..80c87b7 --- /dev/null +++ b/src/main/java/cn/hutool/core/codec/Base32.java @@ -0,0 +1,148 @@ +package cn.hutool.core.codec; + +import cn.hutool.core.util.CharsetUtil; +import cn.hutool.core.util.StrUtil; + +import java.nio.charset.Charset; + +/** + * Base32 - encodes and decodes RFC4648 Base32 (see https://datatracker.ietf.org/doc/html/rfc4648#section-6 )
+ * base32就是用32(2的5次方)个特定ASCII码来表示256个ASCII码。
+ * 所以,5个ASCII字符经过base32编码后会变为8个字符(公约数为40),长度增加3/5.不足8n用“=”补足。
+ * 根据RFC4648 Base32规范,支持两种模式: + *
    + *
  • Base 32 Alphabet (ABCDEFGHIJKLMNOPQRSTUVWXYZ234567)
  • + *
  • "Extended Hex" Base 32 Alphabet (0123456789ABCDEFGHIJKLMNOPQRSTUV)
  • + *
+ * + * @author Looly + */ +public class Base32 { + //----------------------------------------------------------------------------------------- encode + + /** + * 编码 + * + * @param bytes 数据 + * @return base32 + */ + public static String encode(final byte[] bytes) { + return Base32Codec.INSTANCE.encode(bytes); + } + + /** + * base32编码 + * + * @param source 被编码的base32字符串 + * @return 被加密后的字符串 + */ + public static String encode(String source) { + return encode(source, CharsetUtil.CHARSET_UTF_8); + } + + /** + * base32编码 + * + * @param source 被编码的base32字符串 + * @param charset 字符集 + * @return 被加密后的字符串 + */ + public static String encode(String source, Charset charset) { + return encode(StrUtil.bytes(source, charset)); + } + + /** + * 编码 + * + * @param bytes 数据(Hex模式) + * @return base32 + */ + public static String encodeHex(final byte[] bytes) { + return Base32Codec.INSTANCE.encode(bytes, true); + } + + /** + * base32编码(Hex模式) + * + * @param source 被编码的base32字符串 + * @return 被加密后的字符串 + */ + public static String encodeHex(String source) { + return encodeHex(source, CharsetUtil.CHARSET_UTF_8); + } + + /** + * base32编码(Hex模式) + * + * @param source 被编码的base32字符串 + * @param charset 字符集 + * @return 被加密后的字符串 + */ + public static String encodeHex(String source, Charset charset) { + return encodeHex(StrUtil.bytes(source, charset)); + } + + //----------------------------------------------------------------------------------------- decode + + /** + * 解码 + * + * @param base32 base32编码 + * @return 数据 + */ + public static byte[] decode(String base32) { + return Base32Codec.INSTANCE.decode(base32); + } + + /** + * base32解码 + * + * @param source 被解码的base32字符串 + * @return 被加密后的字符串 + */ + public static String decodeStr(String source) { + return decodeStr(source, CharsetUtil.CHARSET_UTF_8); + } + + /** + * base32解码 + * + * @param source 被解码的base32字符串 + * @param charset 字符集 + * @return 被加密后的字符串 + */ + public static String decodeStr(String source, Charset charset) { + return StrUtil.str(decode(source), charset); + } + + /** + * 解码 + * + * @param base32 base32编码 + * @return 数据 + */ + public static byte[] decodeHex(String base32) { + return Base32Codec.INSTANCE.decode(base32, true); + } + + /** + * base32解码 + * + * @param source 被解码的base32字符串 + * @return 被加密后的字符串 + */ + public static String decodeStrHex(String source) { + return decodeStrHex(source, CharsetUtil.CHARSET_UTF_8); + } + + /** + * base32解码 + * + * @param source 被解码的base32字符串 + * @param charset 字符集 + * @return 被加密后的字符串 + */ + public static String decodeStrHex(String source, Charset charset) { + return StrUtil.str(decodeHex(source), charset); + } +} diff --git a/src/main/java/cn/hutool/core/codec/Base32Codec.java b/src/main/java/cn/hutool/core/codec/Base32Codec.java new file mode 100644 index 0000000..42b926e --- /dev/null +++ b/src/main/java/cn/hutool/core/codec/Base32Codec.java @@ -0,0 +1,215 @@ +package cn.hutool.core.codec; + +import java.util.Arrays; + +/** + * Base32 - encodes and decodes RFC4648 Base32 (see https://datatracker.ietf.org/doc/html/rfc4648#section-6 )
+ * base32就是用32(2的5次方)个特定ASCII码来表示256个ASCII码。
+ * 所以,5个ASCII字符经过base32编码后会变为8个字符(公约数为40),长度增加3/5.不足8n用“=”补足。
+ * 根据RFC4648 Base32规范,支持两种模式: + *
    + *
  • Base 32 Alphabet (ABCDEFGHIJKLMNOPQRSTUVWXYZ234567)
  • + *
  • "Extended Hex" Base 32 Alphabet (0123456789ABCDEFGHIJKLMNOPQRSTUV)
  • + *
+ * + * @author Looly + * @since 5.8.0 + */ +public class Base32Codec implements Encoder, Decoder { + + public static Base32Codec INSTANCE = new Base32Codec(); + + @Override + public String encode(byte[] data) { + return encode(data, false); + } + + /** + * 编码数据 + * + * @param data 数据 + * @param useHex 是否使用Hex Alphabet + * @return 编码后的Base32字符串 + */ + public String encode(byte[] data, boolean useHex) { + final Base32Encoder encoder = useHex ? Base32Encoder.HEX_ENCODER : Base32Encoder.ENCODER; + return encoder.encode(data); + } + + @Override + public byte[] decode(CharSequence encoded) { + return decode(encoded, false); + } + + /** + * 解码数据 + * + * @param encoded base32字符串 + * @param useHex 是否使用Hex Alphabet + * @return 解码后的内容 + */ + public byte[] decode(CharSequence encoded, boolean useHex) { + final Base32Decoder decoder = useHex ? Base32Decoder.HEX_DECODER : Base32Decoder.DECODER; + return decoder.decode(encoded); + } + + /** + * Bas32编码器 + */ + public static class Base32Encoder implements Encoder { + private static final String DEFAULT_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"; + private static final String HEX_ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUV"; + private static final Character DEFAULT_PAD = '='; + private static final int[] BASE32_FILL = {-1, 4, 1, 6, 3}; + + public static final Base32Encoder ENCODER = new Base32Encoder(DEFAULT_ALPHABET, DEFAULT_PAD); + public static final Base32Encoder HEX_ENCODER = new Base32Encoder(HEX_ALPHABET, DEFAULT_PAD); + + private final char[] alphabet; + private final Character pad; + + /** + * 构造 + * + * @param alphabet 自定义编码字母表,见 {@link #DEFAULT_ALPHABET}和 {@link #HEX_ALPHABET} + * @param pad 补位字符 + */ + public Base32Encoder(String alphabet, Character pad) { + this.alphabet = alphabet.toCharArray(); + this.pad = pad; + } + + @Override + public String encode(byte[] data) { + int i = 0; + int index = 0; + int digit; + int currByte; + int nextByte; + + int encodeLen = data.length * 8 / 5; + if (encodeLen != 0) { + encodeLen = encodeLen + 1 + BASE32_FILL[(data.length * 8) % 5]; + } + + StringBuilder base32 = new StringBuilder(encodeLen); + + while (i < data.length) { + // unsign + currByte = (data[i] >= 0) ? data[i] : (data[i] + 256); + + /* Is the current digit going to span a byte boundary? */ + if (index > 3) { + if ((i + 1) < data.length) { + nextByte = (data[i + 1] >= 0) ? data[i + 1] : (data[i + 1] + 256); + } else { + nextByte = 0; + } + + digit = currByte & (0xFF >> index); + index = (index + 5) % 8; + digit <<= index; + digit |= nextByte >> (8 - index); + i++; + } else { + digit = (currByte >> (8 - (index + 5))) & 0x1F; + index = (index + 5) % 8; + if (index == 0) { + i++; + } + } + base32.append(alphabet[digit]); + } + + if (null != pad) { + // 末尾补充不足长度的 + while (base32.length() < encodeLen) { + base32.append(pad.charValue()); + } + } + + return base32.toString(); + } + } + + /** + * Base32解码器 + */ + public static class Base32Decoder implements Decoder { + private static final char BASE_CHAR = '0'; + + public static final Base32Decoder DECODER = new Base32Decoder(Base32Encoder.DEFAULT_ALPHABET); + public static final Base32Decoder HEX_DECODER = new Base32Decoder(Base32Encoder.HEX_ALPHABET); + + private final byte[] lookupTable; + + /** + * 构造 + * + * @param alphabet 编码字母表 + */ + public Base32Decoder(String alphabet) { + lookupTable = new byte[128]; + Arrays.fill(lookupTable, (byte) -1); + + final int length = alphabet.length(); + + char c; + for (int i = 0; i < length; i++) { + c = alphabet.charAt(i); + lookupTable[c - BASE_CHAR] = (byte) i; + // 支持小写字母解码 + if(c >= 'A' && c <= 'Z'){ + lookupTable[Character.toLowerCase(c) - BASE_CHAR] = (byte) i; + } + } + } + + @Override + public byte[] decode(CharSequence encoded) { + int i, index, lookup, offset, digit; + final String base32 = encoded.toString(); + int len = base32.endsWith("=") ? base32.indexOf("=") * 5 / 8 : base32.length() * 5 / 8; + byte[] bytes = new byte[len]; + + for (i = 0, index = 0, offset = 0; i < base32.length(); i++) { + lookup = base32.charAt(i) - BASE_CHAR; + + /* Skip chars outside the lookup table */ + if (lookup < 0 || lookup >= lookupTable.length) { + continue; + } + + digit = lookupTable[lookup]; + + /* If this digit is not in the table, ignore it */ + if (digit < 0) { + continue; + } + + if (index <= 3) { + index = (index + 5) % 8; + if (index == 0) { + bytes[offset] |= digit; + offset++; + if (offset >= bytes.length) { + break; + } + } else { + bytes[offset] |= digit << (8 - index); + } + } else { + index = (index + 5) % 8; + bytes[offset] |= (digit >>> index); + offset++; + + if (offset >= bytes.length) { + break; + } + bytes[offset] |= digit << (8 - index); + } + } + return bytes; + } + } +} diff --git a/src/main/java/cn/hutool/core/codec/Base58.java b/src/main/java/cn/hutool/core/codec/Base58.java new file mode 100644 index 0000000..c1c15f4 --- /dev/null +++ b/src/main/java/cn/hutool/core/codec/Base58.java @@ -0,0 +1,152 @@ +package cn.hutool.core.codec; + +import cn.hutool.core.exceptions.UtilException; +import cn.hutool.core.exceptions.ValidateException; + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Arrays; + + +/** + * Base58工具类,提供Base58的编码和解码方案
+ * 参考: https://github.com/Anujraval24/Base58Encoding
+ * 规范见:https://en.bitcoin.it/wiki/Base58Check_encoding + * + * @author lin, looly + * @since 5.7.22 + */ +public class Base58 { + + private static final int CHECKSUM_SIZE = 4; + + // -------------------------------------------------------------------- encode + + /** + * Base58编码
+ * 包含版本位和校验位 + * + * @param version 编码版本,{@code null}表示不包含版本位 + * @param data 被编码的数组,添加校验和。 + * @return 编码后的字符串 + */ + public static String encodeChecked(Integer version, byte[] data) { + return encode(addChecksum(version, data)); + } + + /** + * Base58编码 + * + * @param data 被编码的数据,不带校验和。 + * @return 编码后的字符串 + */ + public static String encode(byte[] data) { + return Base58Codec.INSTANCE.encode(data); + } + // -------------------------------------------------------------------- decode + + /** + * Base58解码
+ * 解码包含标志位验证和版本呢位去除 + * + * @param encoded 被解码的base58字符串 + * @return 解码后的bytes + * @throws ValidateException 标志位验证错误抛出此异常 + */ + public static byte[] decodeChecked(CharSequence encoded) throws ValidateException { + try { + return decodeChecked(encoded, true); + } catch (ValidateException ignore) { + return decodeChecked(encoded, false); + } + } + + /** + * Base58解码
+ * 解码包含标志位验证和版本呢位去除 + * + * @param encoded 被解码的base58字符串 + * @param withVersion 是否包含版本位 + * @return 解码后的bytes + * @throws ValidateException 标志位验证错误抛出此异常 + */ + public static byte[] decodeChecked(CharSequence encoded, boolean withVersion) throws ValidateException { + byte[] valueWithChecksum = decode(encoded); + return verifyAndRemoveChecksum(valueWithChecksum, withVersion); + } + + /** + * Base58解码 + * + * @param encoded 被编码的base58字符串 + * @return 解码后的bytes + */ + public static byte[] decode(CharSequence encoded) { + return Base58Codec.INSTANCE.decode(encoded); + } + + /** + * 验证并去除验证位和版本位 + * + * @param data 编码的数据 + * @param withVersion 是否包含版本位 + * @return 载荷数据 + */ + private static byte[] verifyAndRemoveChecksum(byte[] data, boolean withVersion) { + final byte[] payload = Arrays.copyOfRange(data, withVersion ? 1 : 0, data.length - CHECKSUM_SIZE); + final byte[] checksum = Arrays.copyOfRange(data, data.length - CHECKSUM_SIZE, data.length); + final byte[] expectedChecksum = checksum(payload); + if (!Arrays.equals(checksum, expectedChecksum)) { + throw new ValidateException("Base58 checksum is invalid"); + } + return payload; + } + + /** + * 数据 + 校验码 + * + * @param version 版本,{@code null}表示不添加版本位 + * @param payload Base58数据(不含校验码) + * @return Base58数据 + */ + private static byte[] addChecksum(Integer version, byte[] payload) { + final byte[] addressBytes; + if (null != version) { + addressBytes = new byte[1 + payload.length + CHECKSUM_SIZE]; + addressBytes[0] = (byte) version.intValue(); + System.arraycopy(payload, 0, addressBytes, 1, payload.length); + } else { + addressBytes = new byte[payload.length + CHECKSUM_SIZE]; + System.arraycopy(payload, 0, addressBytes, 0, payload.length); + } + final byte[] checksum = checksum(payload); + System.arraycopy(checksum, 0, addressBytes, addressBytes.length - CHECKSUM_SIZE, CHECKSUM_SIZE); + return addressBytes; + } + + /** + * 获取校验码
+ * 计算规则为对数据进行两次sha256计算,然后取{@link #CHECKSUM_SIZE}长度 + * + * @param data 数据 + * @return 校验码 + */ + private static byte[] checksum(byte[] data) { + byte[] hash = hash256(hash256(data)); + return Arrays.copyOfRange(hash, 0, CHECKSUM_SIZE); + } + + /** + * 计算数据的SHA-256值 + * + * @param data 数据 + * @return sha-256值 + */ + private static byte[] hash256(byte[] data) { + try { + return MessageDigest.getInstance("SHA-256").digest(data); + } catch (NoSuchAlgorithmException e) { + throw new UtilException(e); + } + } +} diff --git a/src/main/java/cn/hutool/core/codec/Base58Codec.java b/src/main/java/cn/hutool/core/codec/Base58Codec.java new file mode 100644 index 0000000..da27873 --- /dev/null +++ b/src/main/java/cn/hutool/core/codec/Base58Codec.java @@ -0,0 +1,187 @@ +package cn.hutool.core.codec; + +import cn.hutool.core.util.StrUtil; + +import java.util.Arrays; + +/** + * Base58编码器
+ * 此编码器不包括校验码、版本等信息 + * + * @author lin, looly + * @since 5.7.22 + */ +public class Base58Codec implements Encoder, Decoder { + + public static Base58Codec INSTANCE = new Base58Codec(); + + /** + * Base58编码 + * + * @param data 被编码的数据,不带校验和。 + * @return 编码后的字符串 + */ + @Override + public String encode(byte[] data) { + return Base58Encoder.ENCODER.encode(data); + } + + /** + * 解码给定的Base58字符串 + * + * @param encoded Base58编码字符串 + * @return 解码后的bytes + * @throws IllegalArgumentException 非标准Base58字符串 + */ + @Override + public byte[] decode(CharSequence encoded) throws IllegalArgumentException { + return Base58Decoder.DECODER.decode(encoded); + } + + /** + * Base58编码器 + * + * @since 5.8.0 + */ + public static class Base58Encoder implements Encoder { + private static final String DEFAULT_ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"; + + public static final Base58Encoder ENCODER = new Base58Encoder(DEFAULT_ALPHABET.toCharArray()); + + private final char[] alphabet; + private final char alphabetZero; + + /** + * 构造 + * + * @param alphabet 编码字母表 + */ + public Base58Encoder(char[] alphabet) { + this.alphabet = alphabet; + alphabetZero = alphabet[0]; + } + + @Override + public String encode(byte[] data) { + if (null == data) { + return null; + } + if (data.length == 0) { + return StrUtil.EMPTY; + } + // 计算开头0的个数 + int zeroCount = 0; + while (zeroCount < data.length && data[zeroCount] == 0) { + ++zeroCount; + } + // 将256位编码转换为58位编码 + data = Arrays.copyOf(data, data.length); // since we modify it in-place + final char[] encoded = new char[data.length * 2]; // upper bound + int outputStart = encoded.length; + for (int inputStart = zeroCount; inputStart < data.length; ) { + encoded[--outputStart] = alphabet[divmod(data, inputStart, 256, 58)]; + if (data[inputStart] == 0) { + ++inputStart; // optimization - skip leading zeros + } + } + // Preserve exactly as many leading encoded zeros in output as there were leading zeros in input. + while (outputStart < encoded.length && encoded[outputStart] == alphabetZero) { + ++outputStart; + } + while (--zeroCount >= 0) { + encoded[--outputStart] = alphabetZero; + } + // Return encoded string (including encoded leading zeros). + return new String(encoded, outputStart, encoded.length - outputStart); + } + } + + /** + * Base58解码器 + * + * @since 5.8.0 + */ + public static class Base58Decoder implements Decoder { + + public static Base58Decoder DECODER = new Base58Decoder(Base58Encoder.DEFAULT_ALPHABET); + + private final byte[] lookupTable; + + /** + * 构造 + * + * @param alphabet 编码字符表 + */ + public Base58Decoder(String alphabet) { + final byte[] lookupTable = new byte['z' + 1]; + Arrays.fill(lookupTable, (byte) -1); + + final int length = alphabet.length(); + for (int i = 0; i < length; i++) { + lookupTable[alphabet.charAt(i)] = (byte) i; + } + this.lookupTable = lookupTable; + } + + @Override + public byte[] decode(CharSequence encoded) { + if (encoded.length() == 0) { + return new byte[0]; + } + // Convert the base58-encoded ASCII chars to a base58 byte sequence (base58 digits). + final byte[] input58 = new byte[encoded.length()]; + for (int i = 0; i < encoded.length(); ++i) { + char c = encoded.charAt(i); + int digit = c < 128 ? lookupTable[c] : -1; + if (digit < 0) { + throw new IllegalArgumentException(StrUtil.format("Invalid char '{}' at [{}]", c, i)); + } + input58[i] = (byte) digit; + } + // Count leading zeros. + int zeros = 0; + while (zeros < input58.length && input58[zeros] == 0) { + ++zeros; + } + // Convert base-58 digits to base-256 digits. + byte[] decoded = new byte[encoded.length()]; + int outputStart = decoded.length; + for (int inputStart = zeros; inputStart < input58.length; ) { + decoded[--outputStart] = divmod(input58, inputStart, 58, 256); + if (input58[inputStart] == 0) { + ++inputStart; // optimization - skip leading zeros + } + } + // Ignore extra leading zeroes that were added during the calculation. + while (outputStart < decoded.length && decoded[outputStart] == 0) { + ++outputStart; + } + // Return decoded data (including original number of leading zeros). + return Arrays.copyOfRange(decoded, outputStart - zeros, decoded.length); + } + } + + /** + * Divides a number, represented as an array of bytes each containing a single digit + * in the specified base, by the given divisor. The given number is modified in-place + * to contain the quotient, and the return value is the remainder. + * + * @param number the number to divide + * @param firstDigit the index within the array of the first non-zero digit + * (this is used for optimization by skipping the leading zeros) + * @param base the base in which the number's digits are represented (up to 256) + * @param divisor the number to divide by (up to 256) + * @return the remainder of the division operation + */ + private static byte divmod(byte[] number, int firstDigit, int base, int divisor) { + // this is just long division which accounts for the base of the input digits + int remainder = 0; + for (int i = firstDigit; i < number.length; i++) { + int digit = (int) number[i] & 0xFF; + int temp = remainder * base + digit; + number[i] = (byte) (temp / divisor); + remainder = temp % divisor; + } + return (byte) remainder; + } +} diff --git a/src/main/java/cn/hutool/core/codec/Base62.java b/src/main/java/cn/hutool/core/codec/Base62.java new file mode 100644 index 0000000..9f8a458 --- /dev/null +++ b/src/main/java/cn/hutool/core/codec/Base62.java @@ -0,0 +1,262 @@ +package cn.hutool.core.codec; + +import cn.hutool.core.io.FileUtil; +import cn.hutool.core.io.IoUtil; +import cn.hutool.core.util.CharsetUtil; +import cn.hutool.core.util.StrUtil; + +import java.io.File; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.charset.Charset; + +/** + * Base62工具类,提供Base62的编码和解码方案
+ * + * @author Looly + * @since 4.5.9 + */ +public class Base62 { + + private static final Charset DEFAULT_CHARSET = CharsetUtil.CHARSET_UTF_8; + + // -------------------------------------------------------------------- encode + /** + * Base62编码 + * + * @param source 被编码的Base62字符串 + * @return 被加密后的字符串 + */ + public static String encode(CharSequence source) { + return encode(source, DEFAULT_CHARSET); + } + + /** + * Base62编码 + * + * @param source 被编码的Base62字符串 + * @param charset 字符集 + * @return 被加密后的字符串 + */ + public static String encode(CharSequence source, Charset charset) { + return encode(StrUtil.bytes(source, charset)); + } + + /** + * Base62编码 + * + * @param source 被编码的Base62字符串 + * @return 被加密后的字符串 + */ + public static String encode(byte[] source) { + return new String(Base62Codec.INSTANCE.encode(source)); + } + + /** + * Base62编码 + * + * @param in 被编码Base62的流(一般为图片流或者文件流) + * @return 被加密后的字符串 + */ + public static String encode(InputStream in) { + return encode(IoUtil.readBytes(in)); + } + + /** + * Base62编码 + * + * @param file 被编码Base62的文件 + * @return 被加密后的字符串 + */ + public static String encode(File file) { + return encode(FileUtil.readBytes(file)); + } + + /** + * Base62编码(反转字母表模式) + * + * @param source 被编码的Base62字符串 + * @return 被加密后的字符串 + */ + public static String encodeInverted(CharSequence source) { + return encodeInverted(source, DEFAULT_CHARSET); + } + + /** + * Base62编码(反转字母表模式) + * + * @param source 被编码的Base62字符串 + * @param charset 字符集 + * @return 被加密后的字符串 + */ + public static String encodeInverted(CharSequence source, Charset charset) { + return encodeInverted(StrUtil.bytes(source, charset)); + } + + /** + * Base62编码(反转字母表模式) + * + * @param source 被编码的Base62字符串 + * @return 被加密后的字符串 + */ + public static String encodeInverted(byte[] source) { + return new String(Base62Codec.INSTANCE.encode(source, true)); + } + + /** + * Base62编码 + * + * @param in 被编码Base62的流(一般为图片流或者文件流) + * @return 被加密后的字符串 + */ + public static String encodeInverted(InputStream in) { + return encodeInverted(IoUtil.readBytes(in)); + } + + /** + * Base62编码(反转字母表模式) + * + * @param file 被编码Base62的文件 + * @return 被加密后的字符串 + */ + public static String encodeInverted(File file) { + return encodeInverted(FileUtil.readBytes(file)); + } + + // -------------------------------------------------------------------- decode + /** + * Base62解码 + * + * @param source 被解码的Base62字符串 + * @return 被加密后的字符串 + */ + public static String decodeStrGbk(CharSequence source) { + return decodeStr(source, CharsetUtil.CHARSET_GBK); + } + + /** + * Base62解码 + * + * @param source 被解码的Base62字符串 + * @return 被加密后的字符串 + */ + public static String decodeStr(CharSequence source) { + return decodeStr(source, DEFAULT_CHARSET); + } + + /** + * Base62解码 + * + * @param source 被解码的Base62字符串 + * @param charset 字符集 + * @return 被加密后的字符串 + */ + public static String decodeStr(CharSequence source, Charset charset) { + return StrUtil.str(decode(source), charset); + } + + /** + * Base62解码 + * + * @param Base62 被解码的Base62字符串 + * @param destFile 目标文件 + * @return 目标文件 + */ + public static File decodeToFile(CharSequence Base62, File destFile) { + return FileUtil.writeBytes(decode(Base62), destFile); + } + + /** + * Base62解码 + * + * @param base62Str 被解码的Base62字符串 + * @param out 写出到的流 + * @param isCloseOut 是否关闭输出流 + */ + public static void decodeToStream(CharSequence base62Str, OutputStream out, boolean isCloseOut) { + IoUtil.write(out, isCloseOut, decode(base62Str)); + } + + /** + * Base62解码 + * + * @param base62Str 被解码的Base62字符串 + * @return 被加密后的字符串 + */ + public static byte[] decode(CharSequence base62Str) { + return decode(StrUtil.bytes(base62Str, DEFAULT_CHARSET)); + } + + /** + * 解码Base62 + * + * @param base62bytes Base62输入 + * @return 解码后的bytes + */ + public static byte[] decode(byte[] base62bytes) { + return Base62Codec.INSTANCE.decode(base62bytes); + } + + /** + * Base62解码(反转字母表模式) + * + * @param source 被解码的Base62字符串 + * @return 被加密后的字符串 + */ + public static String decodeStrInverted(CharSequence source) { + return decodeStrInverted(source, DEFAULT_CHARSET); + } + + /** + * Base62解码(反转字母表模式) + * + * @param source 被解码的Base62字符串 + * @param charset 字符集 + * @return 被加密后的字符串 + */ + public static String decodeStrInverted(CharSequence source, Charset charset) { + return StrUtil.str(decodeInverted(source), charset); + } + + /** + * Base62解码(反转字母表模式) + * + * @param Base62 被解码的Base62字符串 + * @param destFile 目标文件 + * @return 目标文件 + */ + public static File decodeToFileInverted(CharSequence Base62, File destFile) { + return FileUtil.writeBytes(decodeInverted(Base62), destFile); + } + + /** + * Base62解码(反转字母表模式) + * + * @param base62Str 被解码的Base62字符串 + * @param out 写出到的流 + * @param isCloseOut 是否关闭输出流 + */ + public static void decodeToStreamInverted(CharSequence base62Str, OutputStream out, boolean isCloseOut) { + IoUtil.write(out, isCloseOut, decodeInverted(base62Str)); + } + + /** + * Base62解码(反转字母表模式) + * + * @param base62Str 被解码的Base62字符串 + * @return 被加密后的字符串 + */ + public static byte[] decodeInverted(CharSequence base62Str) { + return decodeInverted(StrUtil.bytes(base62Str, DEFAULT_CHARSET)); + } + + /** + * 解码Base62(反转字母表模式) + * + * @param base62bytes Base62输入 + * @return 解码后的bytes + */ + public static byte[] decodeInverted(byte[] base62bytes) { + return Base62Codec.INSTANCE.decode(base62bytes, true); + } +} diff --git a/src/main/java/cn/hutool/core/codec/Base62Codec.java b/src/main/java/cn/hutool/core/codec/Base62Codec.java new file mode 100644 index 0000000..055800d --- /dev/null +++ b/src/main/java/cn/hutool/core/codec/Base62Codec.java @@ -0,0 +1,232 @@ +package cn.hutool.core.codec; + +import cn.hutool.core.util.ArrayUtil; + +import java.io.ByteArrayOutputStream; +import java.io.Serializable; + +/** + * Base62编码解码实现,常用于短URL
+ * From https://github.com/seruco/base62 + * + * @author Looly, Sebastian Ruhleder, sebastian@seruco.io + * @since 4.5.9 + */ +public class Base62Codec implements Encoder, Decoder, Serializable { + private static final long serialVersionUID = 1L; + + private static final int STANDARD_BASE = 256; + private static final int TARGET_BASE = 62; + + public static Base62Codec INSTANCE = new Base62Codec(); + + /** + * 编码指定消息bytes为Base62格式的bytes + * + * @param data 被编码的消息 + * @return Base62内容 + */ + @Override + public byte[] encode(byte[] data) { + return encode(data, false); + } + + /** + * 编码指定消息bytes为Base62格式的bytes + * + * @param data 被编码的消息 + * @param useInverted 是否使用反转风格,即将GMP风格中的大小写做转换 + * @return Base62内容 + */ + public byte[] encode(byte[] data, boolean useInverted) { + final Base62Encoder encoder = useInverted ? Base62Encoder.INVERTED_ENCODER : Base62Encoder.GMP_ENCODER; + return encoder.encode(data); + } + + /** + * 解码Base62消息 + * + * @param encoded Base62内容 + * @return 消息 + */ + @Override + public byte[] decode(byte[] encoded) { + return decode(encoded, false); + } + + /** + * 解码Base62消息 + * + * @param encoded Base62内容 + * @param useInverted 是否使用反转风格,即将GMP风格中的大小写做转换 + * @return 消息 + */ + public byte[] decode(byte[] encoded, boolean useInverted) { + final Base62Decoder decoder = useInverted ? Base62Decoder.INVERTED_DECODER : Base62Decoder.GMP_DECODER; + return decoder.decode(encoded); + } + + /** + * Base62编码器 + * + * @since 5.8.0 + */ + public static class Base62Encoder implements Encoder { + /** + * GMP风格 + */ + private static final byte[] GMP = { // + '0', '1', '2', '3', '4', '5', '6', '7', // + '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', // + 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', // + 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', // + 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', // + 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', // + 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', // + 'u', 'v', 'w', 'x', 'y', 'z' // + }; + + /** + * 反转风格,即将GMP风格中的大小写做转换 + */ + private static final byte[] INVERTED = { // + '0', '1', '2', '3', '4', '5', '6', '7', // + '8', '9', 'a', 'b', 'c', 'd', 'e', 'f', // + 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', // + 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', // + 'w', 'x', 'y', 'z', 'A', 'B', 'C', 'D', // + 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', // + 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', // + 'U', 'V', 'W', 'X', 'Y', 'Z' // + }; + + public static Base62Encoder GMP_ENCODER = new Base62Encoder(GMP); + public static Base62Encoder INVERTED_ENCODER = new Base62Encoder(INVERTED); + + private final byte[] alphabet; + + /** + * 构造 + * + * @param alphabet 字符表 + */ + public Base62Encoder(byte[] alphabet) { + this.alphabet = alphabet; + } + + @Override + public byte[] encode(byte[] data) { + final byte[] indices = convert(data, STANDARD_BASE, TARGET_BASE); + return translate(indices, alphabet); + } + } + + /** + * Base62解码器 + * + * @since 5.8.0 + */ + public static class Base62Decoder implements Decoder { + + public static Base62Decoder GMP_DECODER = new Base62Decoder(Base62Encoder.GMP); + public static Base62Decoder INVERTED_DECODER = new Base62Decoder(Base62Encoder.INVERTED); + + private final byte[] lookupTable; + + /** + * 构造 + * + * @param alphabet 字母表 + */ + public Base62Decoder(byte[] alphabet) { + lookupTable = new byte['z' + 1]; + for (int i = 0; i < alphabet.length; i++) { + lookupTable[alphabet[i]] = (byte) i; + } + } + + + @Override + public byte[] decode(byte[] encoded) { + final byte[] prepared = translate(encoded, lookupTable); + return convert(prepared, TARGET_BASE, STANDARD_BASE); + } + } + + // region Private Methods + + /** + * 按照字典转换bytes + * + * @param indices 内容 + * @param dictionary 字典 + * @return 转换值 + */ + private static byte[] translate(byte[] indices, byte[] dictionary) { + final byte[] translation = new byte[indices.length]; + + for (int i = 0; i < indices.length; i++) { + translation[i] = dictionary[indices[i]]; + } + + return translation; + } + + /** + * 使用定义的字母表从源基准到目标基准 + * + * @param message 消息bytes + * @param sourceBase 源基准长度 + * @param targetBase 目标基准长度 + * @return 计算结果 + */ + private static byte[] convert(byte[] message, int sourceBase, int targetBase) { + // 计算结果长度,算法来自:http://codegolf.stackexchange.com/a/21672 + final int estimatedLength = estimateOutputLength(message.length, sourceBase, targetBase); + + final ByteArrayOutputStream out = new ByteArrayOutputStream(estimatedLength); + + byte[] source = message; + + while (source.length > 0) { + final ByteArrayOutputStream quotient = new ByteArrayOutputStream(source.length); + + int remainder = 0; + + for (byte b : source) { + final int accumulator = (b & 0xFF) + remainder * sourceBase; + final int digit = (accumulator - (accumulator % targetBase)) / targetBase; + + remainder = accumulator % targetBase; + + if (quotient.size() > 0 || digit > 0) { + quotient.write(digit); + } + } + + out.write(remainder); + + source = quotient.toByteArray(); + } + + // pad output with zeroes corresponding to the number of leading zeroes in the message + for (int i = 0; i < message.length - 1 && message[i] == 0; i++) { + out.write(0); + } + + return ArrayUtil.reverse(out.toByteArray()); + } + + /** + * 估算结果长度 + * + * @param inputLength 输入长度 + * @param sourceBase 源基准长度 + * @param targetBase 目标基准长度 + * @return 估算长度 + */ + private static int estimateOutputLength(int inputLength, int sourceBase, int targetBase) { + return (int) Math.ceil((Math.log(sourceBase) / Math.log(targetBase)) * inputLength); + } + // endregion +} diff --git a/src/main/java/cn/hutool/core/codec/Base64.java b/src/main/java/cn/hutool/core/codec/Base64.java new file mode 100644 index 0000000..4fc9d28 --- /dev/null +++ b/src/main/java/cn/hutool/core/codec/Base64.java @@ -0,0 +1,386 @@ +package cn.hutool.core.codec; + +import cn.hutool.core.io.FileUtil; +import cn.hutool.core.io.IoUtil; +import cn.hutool.core.util.CharsetUtil; +import cn.hutool.core.util.StrUtil; + +import java.io.File; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.charset.Charset; + +/** + * Base64工具类,提供Base64的编码和解码方案
+ * base64编码是用64(2的6次方)个ASCII字符来表示256(2的8次方)个ASCII字符,
+ * 也就是三位二进制数组经过编码后变为四位的ASCII字符显示,长度比原来增加1/3。 + * + * @author Looly + */ +public class Base64 { + + private static final Charset DEFAULT_CHARSET = CharsetUtil.CHARSET_UTF_8; + // -------------------------------------------------------------------- encode + + /** + * 编码为Base64,非URL安全的 + * + * @param arr 被编码的数组 + * @param lineSep 在76个char之后是CRLF还是EOF + * @return 编码后的bytes + */ + public static byte[] encode(byte[] arr, boolean lineSep) { + return lineSep ? + java.util.Base64.getMimeEncoder().encode(arr) : + java.util.Base64.getEncoder().encode(arr); + } + + /** + * 编码为Base64,URL安全的 + * + * @param arr 被编码的数组 + * @param lineSep 在76个char之后是CRLF还是EOF + * @return 编码后的bytes + * @since 3.0.6 + * @deprecated 按照RFC2045规范,URL安全的Base64无需换行 + */ + @Deprecated + public static byte[] encodeUrlSafe(byte[] arr, boolean lineSep) { + return Base64Encoder.encodeUrlSafe(arr, lineSep); + } + + /** + * base64编码 + * + * @param source 被编码的base64字符串 + * @return 被加密后的字符串 + */ + public static String encode(CharSequence source) { + return encode(source, DEFAULT_CHARSET); + } + + /** + * base64编码,URL安全 + * + * @param source 被编码的base64字符串 + * @return 被加密后的字符串 + * @since 3.0.6 + */ + public static String encodeUrlSafe(CharSequence source) { + return encodeUrlSafe(source, DEFAULT_CHARSET); + } + + /** + * base64编码 + * + * @param source 被编码的base64字符串 + * @param charset 字符集 + * @return 被加密后的字符串 + */ + public static String encode(CharSequence source, String charset) { + return encode(source, CharsetUtil.charset(charset)); + } + + /** + * base64编码,不进行padding(末尾不会填充'=') + * + * @param source 被编码的base64字符串 + * @param charset 编码 + * @return 被加密后的字符串 + * @since 5.5.2 + */ + public static String encodeWithoutPadding(CharSequence source, String charset) { + return encodeWithoutPadding(StrUtil.bytes(source, charset)); + } + + /** + * base64编码,URL安全 + * + * @param source 被编码的base64字符串 + * @param charset 字符集 + * @return 被加密后的字符串 + * @since 3.0.6 + * @deprecated 请使用 {@link #encodeUrlSafe(CharSequence, Charset)} + */ + @Deprecated + public static String encodeUrlSafe(CharSequence source, String charset) { + return encodeUrlSafe(source, CharsetUtil.charset(charset)); + } + + /** + * base64编码 + * + * @param source 被编码的base64字符串 + * @param charset 字符集 + * @return 被编码后的字符串 + */ + public static String encode(CharSequence source, Charset charset) { + return encode(StrUtil.bytes(source, charset)); + } + + /** + * base64编码,URL安全的 + * + * @param source 被编码的base64字符串 + * @param charset 字符集 + * @return 被加密后的字符串 + * @since 3.0.6 + */ + public static String encodeUrlSafe(CharSequence source, Charset charset) { + return encodeUrlSafe(StrUtil.bytes(source, charset)); + } + + /** + * base64编码 + * + * @param source 被编码的base64字符串 + * @return 被加密后的字符串 + */ + public static String encode(byte[] source) { + return java.util.Base64.getEncoder().encodeToString(source); + } + + /** + * base64编码,不进行padding(末尾不会填充'=') + * + * @param source 被编码的base64字符串 + * @return 被加密后的字符串 + * @since 5.5.2 + */ + public static String encodeWithoutPadding(byte[] source) { + return java.util.Base64.getEncoder().withoutPadding().encodeToString(source); + } + + /** + * base64编码,URL安全的 + * + * @param source 被编码的base64字符串 + * @return 被加密后的字符串 + * @since 3.0.6 + */ + public static String encodeUrlSafe(byte[] source) { + return java.util.Base64.getUrlEncoder().withoutPadding().encodeToString(source); + } + + /** + * base64编码 + * + * @param in 被编码base64的流(一般为图片流或者文件流) + * @return 被加密后的字符串 + * @since 4.0.9 + */ + public static String encode(InputStream in) { + return encode(IoUtil.readBytes(in)); + } + + /** + * base64编码,URL安全的 + * + * @param in 被编码base64的流(一般为图片流或者文件流) + * @return 被加密后的字符串 + * @since 4.0.9 + */ + public static String encodeUrlSafe(InputStream in) { + return encodeUrlSafe(IoUtil.readBytes(in)); + } + + /** + * base64编码 + * + * @param file 被编码base64的文件 + * @return 被加密后的字符串 + * @since 4.0.9 + */ + public static String encode(File file) { + return encode(FileUtil.readBytes(file)); + } + + /** + * base64编码,URL安全的 + * + * @param file 被编码base64的文件 + * @return 被加密后的字符串 + * @since 4.0.9 + */ + public static String encodeUrlSafe(File file) { + return encodeUrlSafe(FileUtil.readBytes(file)); + } + + /** + * 编码为Base64字符串
+ * 如果isMultiLine为{@code true},则每76个字符一个换行符,否则在一行显示 + * + * @param arr 被编码的数组 + * @param isMultiLine 在76个char之后是CRLF还是EOF + * @param isUrlSafe 是否使用URL安全字符,一般为{@code false} + * @return 编码后的bytes + * @since 5.7.2 + */ + public static String encodeStr(byte[] arr, boolean isMultiLine, boolean isUrlSafe) { + return StrUtil.str(encode(arr, isMultiLine, isUrlSafe), DEFAULT_CHARSET); + } + + /** + * 编码为Base64
+ * 如果isMultiLine为{@code true},则每76个字符一个换行符,否则在一行显示 + * + * @param arr 被编码的数组 + * @param isMultiLine 在76个char之后是CRLF还是EOF + * @param isUrlSafe 是否使用URL安全字符,一般为{@code false} + * @return 编码后的bytes + */ + public static byte[] encode(byte[] arr, boolean isMultiLine, boolean isUrlSafe) { + return Base64Encoder.encode(arr, isMultiLine, isUrlSafe); + } + + // -------------------------------------------------------------------- decode + + /** + * base64解码 + * + * @param source 被解码的base64字符串 + * @return 被加密后的字符串 + * @since 4.3.2 + */ + public static String decodeStrGbk(CharSequence source) { + return Base64Decoder.decodeStr(source, CharsetUtil.CHARSET_GBK); + } + + /** + * base64解码 + * + * @param source 被解码的base64字符串 + * @return 被加密后的字符串 + */ + public static String decodeStr(CharSequence source) { + return Base64Decoder.decodeStr(source); + } + + /** + * base64解码 + * + * @param source 被解码的base64字符串 + * @param charset 字符集 + * @return 被加密后的字符串 + */ + public static String decodeStr(CharSequence source, String charset) { + return decodeStr(source, CharsetUtil.charset(charset)); + } + + /** + * base64解码 + * + * @param source 被解码的base64字符串 + * @param charset 字符集 + * @return 被加密后的字符串 + */ + public static String decodeStr(CharSequence source, Charset charset) { + return Base64Decoder.decodeStr(source, charset); + } + + /** + * base64解码 + * + * @param base64 被解码的base64字符串 + * @param destFile 目标文件 + * @return 目标文件 + * @since 4.0.9 + */ + public static File decodeToFile(CharSequence base64, File destFile) { + return FileUtil.writeBytes(Base64Decoder.decode(base64), destFile); + } + + /** + * base64解码 + * + * @param base64 被解码的base64字符串 + * @param out 写出到的流 + * @param isCloseOut 是否关闭输出流 + * @since 4.0.9 + */ + public static void decodeToStream(CharSequence base64, OutputStream out, boolean isCloseOut) { + IoUtil.write(out, isCloseOut, Base64Decoder.decode(base64)); + } + + /** + * base64解码 + * + * @param base64 被解码的base64字符串 + * @return 解码后的bytes + */ + public static byte[] decode(CharSequence base64) { + return Base64Decoder.decode(base64); + } + + /** + * 解码Base64 + * + * @param in 输入 + * @return 解码后的bytes + */ + public static byte[] decode(byte[] in) { + return Base64Decoder.decode(in); + } + + /** + * 检查是否为Base64 + * + * @param base64 Base64的bytes + * @return 是否为Base64 + * @since 5.7.5 + */ + public static boolean isBase64(CharSequence base64) { + if (base64 == null || base64.length() < 2) { + return false; + } + + final byte[] bytes = StrUtil.utf8Bytes(base64); + + if (bytes.length != base64.length()) { + // 如果长度不相等,说明存在双字节字符,肯定不是Base64,直接返回false + return false; + } + + return isBase64(bytes); + } + + /** + * 检查是否为Base64 + * + * @param base64Bytes Base64的bytes + * @return 是否为Base64 + * @since 5.7.5 + */ + public static boolean isBase64(byte[] base64Bytes) { + if (base64Bytes == null || base64Bytes.length < 3) { + return false; + } + boolean hasPadding = false; + for (byte base64Byte : base64Bytes) { + if (hasPadding) { + if ('=' != base64Byte) { + // 前一个字符是'=',则后边的字符都必须是'=',即'='只能都位于结尾 + return false; + } + } else if ('=' == base64Byte) { + // 发现'=' 标记之 + hasPadding = true; + } else if (!(Base64Decoder.isBase64Code(base64Byte) || isWhiteSpace(base64Byte))) { + return false; + } + } + return true; + } + + private static boolean isWhiteSpace(byte byteToCheck) { + switch (byteToCheck) { + case ' ': + case '\n': + case '\r': + case '\t': + return true; + default: + return false; + } + } +} diff --git a/src/main/java/cn/hutool/core/codec/Base64Decoder.java b/src/main/java/cn/hutool/core/codec/Base64Decoder.java new file mode 100644 index 0000000..92b95cb --- /dev/null +++ b/src/main/java/cn/hutool/core/codec/Base64Decoder.java @@ -0,0 +1,162 @@ +package cn.hutool.core.codec; + +import cn.hutool.core.lang.mutable.MutableInt; +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.CharsetUtil; +import cn.hutool.core.util.StrUtil; + +import java.nio.charset.Charset; + +/** + * Base64解码实现 + * + * @author looly + * + */ +public class Base64Decoder { + + private static final Charset DEFAULT_CHARSET = CharsetUtil.CHARSET_UTF_8; + private static final byte PADDING = -2; + + /** Base64解码表,共128位,-1表示非base64字符,-2表示padding */ + private static final byte[] DECODE_TABLE = { + // 0 1 2 3 4 5 6 7 8 9 A B C D E F + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 00-0f + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 10-1f + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, 62, -1, 63, // 20-2f + - / + 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -2, -1, -1, // 30-3f 0-9,-2的位置是'=' + -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, // 40-4f A-O + 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, 63, // 50-5f P-Z _ + -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, // 60-6f a-o + 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51 // 70-7a p-z + }; + + /** + * base64解码 + * + * @param source 被解码的base64字符串 + * @return 被加密后的字符串 + */ + public static String decodeStr(CharSequence source) { + return decodeStr(source, DEFAULT_CHARSET); + } + + /** + * base64解码 + * + * @param source 被解码的base64字符串 + * @param charset 字符集 + * @return 被加密后的字符串 + */ + public static String decodeStr(CharSequence source, Charset charset) { + return StrUtil.str(decode(source), charset); + } + + /** + * base64解码 + * + * @param source 被解码的base64字符串 + * @return 被加密后的字符串 + */ + public static byte[] decode(CharSequence source) { + return decode(StrUtil.bytes(source, DEFAULT_CHARSET)); + } + + /** + * 解码Base64 + * + * @param in 输入 + * @return 解码后的bytes + */ + public static byte[] decode(byte[] in) { + if (ArrayUtil.isEmpty(in)) { + return in; + } + return decode(in, 0, in.length); + } + + /** + * 解码Base64 + * + * @param in 输入 + * @param pos 开始位置 + * @param length 长度 + * @return 解码后的bytes + */ + public static byte[] decode(byte[] in, int pos, int length) { + if (ArrayUtil.isEmpty(in)) { + return in; + } + + final MutableInt offset = new MutableInt(pos); + + byte sestet0; + byte sestet1; + byte sestet2; + byte sestet3; + int maxPos = pos + length - 1; + int octetId = 0; + byte[] octet = new byte[length * 3 / 4];// over-estimated if non-base64 characters present + while (offset.intValue() <= maxPos) { + sestet0 = getNextValidDecodeByte(in, offset, maxPos); + sestet1 = getNextValidDecodeByte(in, offset, maxPos); + sestet2 = getNextValidDecodeByte(in, offset, maxPos); + sestet3 = getNextValidDecodeByte(in, offset, maxPos); + + if (PADDING != sestet1) { + octet[octetId++] = (byte) ((sestet0 << 2) | (sestet1 >>> 4)); + } + if (PADDING != sestet2) { + octet[octetId++] = (byte) (((sestet1 & 0xf) << 4) | (sestet2 >>> 2)); + } + if (PADDING != sestet3) { + octet[octetId++] = (byte) (((sestet2 & 3) << 6) | sestet3); + } + } + + if (octetId == octet.length) { + return octet; + } else { + // 如果有非Base64字符混入,则实际结果比解析的要短,截取之 + return (byte[]) ArrayUtil.copy(octet, new byte[octetId], octetId); + } + } + + /** + * 给定的字符是否为Base64字符 + * + * @param octet 被检查的字符 + * @return 是否为Base64字符 + * @since 5.7.5 + */ + public static boolean isBase64Code(byte octet) { + return octet == '=' || (octet >= 0 && octet < DECODE_TABLE.length && DECODE_TABLE[octet] != -1); + } + + // ----------------------------------------------------------------------------------------------- Private start + /** + * 获取下一个有效的byte字符 + * + * @param in 输入 + * @param pos 当前位置,调用此方法后此位置保持在有效字符的下一个位置 + * @param maxPos 最大位置 + * @return 有效字符,如果达到末尾返回 + */ + private static byte getNextValidDecodeByte(byte[] in, MutableInt pos, int maxPos) { + byte base64Byte; + byte decodeByte; + while (pos.intValue() <= maxPos) { + base64Byte = in[pos.intValue()]; + pos.increment(); + if (base64Byte > -1) { + decodeByte = DECODE_TABLE[base64Byte]; + if (decodeByte > -1) { + return decodeByte; + } + } + } + // padding if reached max position + return PADDING; + } + // ----------------------------------------------------------------------------------------------- Private end +} diff --git a/src/main/java/cn/hutool/core/codec/Base64Encoder.java b/src/main/java/cn/hutool/core/codec/Base64Encoder.java new file mode 100644 index 0000000..a895322 --- /dev/null +++ b/src/main/java/cn/hutool/core/codec/Base64Encoder.java @@ -0,0 +1,214 @@ +package cn.hutool.core.codec; + +import cn.hutool.core.util.CharsetUtil; +import cn.hutool.core.util.StrUtil; + +import java.nio.charset.Charset; + +/** + * Base64编码
+ * TODO 6.x移除此类,使用JDK自身 + * + * @author looly + * @since 3.2.0 + */ +public class Base64Encoder { + + private static final Charset DEFAULT_CHARSET = CharsetUtil.CHARSET_UTF_8; + /** + * 标准编码表 + */ + private static final byte[] STANDARD_ENCODE_TABLE = { // + 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', // + 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', // + 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', // + 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', // + 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', // + 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', // + 'w', 'x', 'y', 'z', '0', '1', '2', '3', // + '4', '5', '6', '7', '8', '9', '+', '/' // + }; + /** + * URL安全的编码表,将 + 和 / 替换为 - 和 _ + */ + private static final byte[] URL_SAFE_ENCODE_TABLE = { // + 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', // + 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', // + 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', // + 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', // + 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', // + 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', // + 'w', 'x', 'y', 'z', '0', '1', '2', '3', // + '4', '5', '6', '7', '8', '9', '-', '_' // + }; + + // -------------------------------------------------------------------- encode + + /** + * 编码为Base64,非URL安全的 + * + * @param arr 被编码的数组 + * @param lineSep 在76个char之后是CRLF还是EOF + * @return 编码后的bytes + */ + public static byte[] encode(byte[] arr, boolean lineSep) { + return encode(arr, lineSep, false); + } + + /** + * 编码为Base64,URL安全的 + * + * @param arr 被编码的数组 + * @param lineSep 在76个char之后是CRLF还是EOF + * @return 编码后的bytes + * @since 3.0.6 + */ + public static byte[] encodeUrlSafe(byte[] arr, boolean lineSep) { + return encode(arr, lineSep, true); + } + + /** + * base64编码 + * + * @param source 被编码的base64字符串 + * @return 被加密后的字符串 + */ + public static String encode(CharSequence source) { + return encode(source, DEFAULT_CHARSET); + } + + /** + * base64编码,URL安全 + * + * @param source 被编码的base64字符串 + * @return 被加密后的字符串 + * @since 3.0.6 + */ + public static String encodeUrlSafe(CharSequence source) { + return encodeUrlSafe(source, DEFAULT_CHARSET); + } + + /** + * base64编码 + * + * @param source 被编码的base64字符串 + * @param charset 字符集 + * @return 被加密后的字符串 + */ + public static String encode(CharSequence source, Charset charset) { + return encode(StrUtil.bytes(source, charset)); + } + + /** + * base64编码,URL安全的 + * + * @param source 被编码的base64字符串 + * @param charset 字符集 + * @return 被加密后的字符串 + * @since 3.0.6 + */ + public static String encodeUrlSafe(CharSequence source, Charset charset) { + return encodeUrlSafe(StrUtil.bytes(source, charset)); + } + + /** + * base64编码 + * + * @param source 被编码的base64字符串 + * @return 被加密后的字符串 + */ + public static String encode(byte[] source) { + return StrUtil.str(encode(source, false), DEFAULT_CHARSET); + } + + /** + * base64编码,URL安全的 + * + * @param source 被编码的base64字符串 + * @return 被加密后的字符串 + * @since 3.0.6 + */ + public static String encodeUrlSafe(byte[] source) { + return StrUtil.str(encodeUrlSafe(source, false), DEFAULT_CHARSET); + } + + /** + * 编码为Base64字符串
+ * 如果isMultiLine为{@code true},则每76个字符一个换行符,否则在一行显示 + * + * @param arr 被编码的数组 + * @param isMultiLine 在76个char之后是CRLF还是EOF + * @param isUrlSafe 是否使用URL安全字符,在URL Safe模式下,=为URL中的关键字符,不需要补充。空余的byte位要去掉,一般为{@code false} + * @return 编码后的bytes + * @since 5.7.2 + */ + public static String encodeStr(byte[] arr, boolean isMultiLine, boolean isUrlSafe) { + return StrUtil.str(encode(arr, isMultiLine, isUrlSafe), DEFAULT_CHARSET); + } + + /** + * 编码为Base64
+ * 如果isMultiLine为{@code true},则每76个字符一个换行符,否则在一行显示 + * + * @param arr 被编码的数组 + * @param isMultiLine 在76个char之后是CRLF还是EOF + * @param isUrlSafe 是否使用URL安全字符,在URL Safe模式下,=为URL中的关键字符,不需要补充。空余的byte位要去掉,一般为{@code false} + * @return 编码后的bytes + */ + public static byte[] encode(byte[] arr, boolean isMultiLine, boolean isUrlSafe) { + if (null == arr) { + return null; + } + + int len = arr.length; + if (len == 0) { + return new byte[0]; + } + + int evenlen = (len / 3) * 3; + int cnt = ((len - 1) / 3 + 1) << 2; + int destlen = cnt + (isMultiLine ? (cnt - 1) / 76 << 1 : 0); + byte[] dest = new byte[destlen]; + + byte[] encodeTable = isUrlSafe ? URL_SAFE_ENCODE_TABLE : STANDARD_ENCODE_TABLE; + + for (int s = 0, d = 0, cc = 0; s < evenlen; ) { + int i = (arr[s++] & 0xff) << 16 | (arr[s++] & 0xff) << 8 | (arr[s++] & 0xff); + + dest[d++] = encodeTable[(i >>> 18) & 0x3f]; + dest[d++] = encodeTable[(i >>> 12) & 0x3f]; + dest[d++] = encodeTable[(i >>> 6) & 0x3f]; + dest[d++] = encodeTable[i & 0x3f]; + + if (isMultiLine && ++cc == 19 && d < destlen - 2) { + dest[d++] = '\r'; + dest[d++] = '\n'; + cc = 0; + } + } + + int left = len - evenlen;// 剩余位数 + if (left > 0) { + int i = ((arr[evenlen] & 0xff) << 10) | (left == 2 ? ((arr[len - 1] & 0xff) << 2) : 0); + + dest[destlen - 4] = encodeTable[i >> 12]; + dest[destlen - 3] = encodeTable[(i >>> 6) & 0x3f]; + + if (isUrlSafe) { + // 在URL Safe模式下,=为URL中的关键字符,不需要补充。空余的byte位要去掉。 + int urlSafeLen = destlen - 2; + if (2 == left) { + dest[destlen - 2] = encodeTable[i & 0x3f]; + urlSafeLen += 1; + } + byte[] urlSafeDest = new byte[urlSafeLen]; + System.arraycopy(dest, 0, urlSafeDest, 0, urlSafeLen); + return urlSafeDest; + } else { + dest[destlen - 2] = (left == 2) ? encodeTable[i & 0x3f] : (byte) '='; + dest[destlen - 1] = '='; + } + } + return dest; + } +} diff --git a/src/main/java/cn/hutool/core/codec/Caesar.java b/src/main/java/cn/hutool/core/codec/Caesar.java new file mode 100644 index 0000000..6746800 --- /dev/null +++ b/src/main/java/cn/hutool/core/codec/Caesar.java @@ -0,0 +1,89 @@ +package cn.hutool.core.codec; + +import cn.hutool.core.lang.Assert; + +/** + * 凯撒密码实现
+ * 算法来自:https://github.com/zhaorenjie110/SymmetricEncryptionAndDecryption + * + * @author looly + */ +public class Caesar { + + // 26个字母表 + public static final String TABLE = "AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz"; + + /** + * 传入明文,加密得到密文 + * + * @param message 加密的消息 + * @param offset 偏移量 + * @return 加密后的内容 + */ + public static String encode(String message, int offset) { + Assert.notNull(message, "message must be not null!"); + final int len = message.length(); + final char[] plain = message.toCharArray(); + char c; + for (int i = 0; i < len; i++) { + c = message.charAt(i); + if (!Character.isLetter(c)) { + continue; + } + plain[i] = encodeChar(c, offset); + } + return new String(plain); + } + + /** + * 传入明文解密到密文 + * + * @param cipherText 密文 + * @param offset 偏移量 + * @return 解密后的内容 + */ + public static String decode(String cipherText, int offset) { + Assert.notNull(cipherText, "cipherText must be not null!"); + final int len = cipherText.length(); + final char[] plain = cipherText.toCharArray(); + char c; + for (int i = 0; i < len; i++) { + c = cipherText.charAt(i); + if (!Character.isLetter(c)) { + continue; + } + plain[i] = decodeChar(c, offset); + } + return new String(plain); + } + + // ----------------------------------------------------------------------------------------- Private method start + + /** + * 加密轮盘 + * + * @param c 被加密字符 + * @param offset 偏移量 + * @return 加密后的字符 + */ + private static char encodeChar(char c, int offset) { + int position = (TABLE.indexOf(c) + offset) % 52; + return TABLE.charAt(position); + + } + + /** + * 解密轮盘 + * + * @param c 字符 + * @return 解密后的字符 + */ + private static char decodeChar(char c, int offset) { + int position = (TABLE.indexOf(c) - offset) % 52; + if (position < 0) { + position += 52; + } + return TABLE.charAt(position); + } + // ----------------------------------------------------------------------------------------- Private method end +} diff --git a/src/main/java/cn/hutool/core/codec/Decoder.java b/src/main/java/cn/hutool/core/codec/Decoder.java new file mode 100644 index 0000000..5954e17 --- /dev/null +++ b/src/main/java/cn/hutool/core/codec/Decoder.java @@ -0,0 +1,20 @@ +package cn.hutool.core.codec; + +/** + * 解码接口 + * + * @param 被解码的数据类型 + * @param 解码后的数据类型 + * @author looly + * @since 5.7.22 + */ +public interface Decoder { + + /** + * 执行解码 + * + * @param encoded 被解码的数据 + * @return 解码后的数据 + */ + R decode(T encoded); +} diff --git a/src/main/java/cn/hutool/core/codec/Encoder.java b/src/main/java/cn/hutool/core/codec/Encoder.java new file mode 100644 index 0000000..9cc728d --- /dev/null +++ b/src/main/java/cn/hutool/core/codec/Encoder.java @@ -0,0 +1,20 @@ +package cn.hutool.core.codec; + +/** + * 编码接口 + * + * @param 被编码的数据类型 + * @param 编码后的数据类型 + * @author looly + * @since 5.7.22 + */ +public interface Encoder { + + /** + * 执行编码 + * + * @param data 被编码的数据 + * @return 编码后的数据 + */ + R encode(T data); +} diff --git a/src/main/java/cn/hutool/core/codec/Hashids.java b/src/main/java/cn/hutool/core/codec/Hashids.java new file mode 100644 index 0000000..7b82bf4 --- /dev/null +++ b/src/main/java/cn/hutool/core/codec/Hashids.java @@ -0,0 +1,506 @@ +package cn.hutool.core.codec; + +import java.math.BigInteger; +import java.util.Arrays; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import java.util.stream.LongStream; + +/** + * Hashids 协议实现,以实现: + *
    + *
  • 生成简短、唯一、大小写敏感并无序的hash值
  • + *
  • 自然数字的Hash值
  • + *
  • 可以设置不同的盐,具有保密性
  • + *
  • 可配置的hash长度
  • + *
  • 递增的输入产生的输出无法预测
  • + *
+ * + *

+ * 来自:https://github.com/davidafsilva/java-hashids + *

+ * + *

+ * {@code Hashids}可以将数字或者16进制字符串转为短且唯一不连续的字符串,采用双向编码实现,比如,它可以将347之类的数字转换为yr8之类的字符串,也可以将yr8之类的字符串重新解码为347之类的数字。
+ * 此编码算法主要是解决爬虫类应用对连续ID爬取问题,将有序的ID转换为无序的Hashids,而且一一对应。 + *

+ * + * @author david + */ +public class Hashids implements Encoder, Decoder { + + private static final int LOTTERY_MOD = 100; + private static final double GUARD_THRESHOLD = 12; + private static final double SEPARATOR_THRESHOLD = 3.5; + // 最小编解码字符串 + private static final int MIN_ALPHABET_LENGTH = 16; + private static final Pattern HEX_VALUES_PATTERN = Pattern.compile("[\\w\\W]{1,12}"); + + // 默认编解码字符串 + public static final char[] DEFAULT_ALPHABET = { + 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', + 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', + 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', + 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', + '1', '2', '3', '4', '5', '6', '7', '8', '9', '0' + }; + // 默认分隔符 + private static final char[] DEFAULT_SEPARATORS = { + 'c', 'f', 'h', 'i', 's', 't', 'u', 'C', 'F', 'H', 'I', 'S', 'T', 'U' + }; + + // algorithm properties + private final char[] alphabet; + // 多个数字编解码的分界符 + private final char[] separators; + private final Set separatorsSet; + private final char[] salt; + // 补齐至 minLength 长度添加的字符列表 + private final char[] guards; + // 编码后最小的字符长度 + private final int minLength; + + // region create + + /** + * 根据参数值,创建{@code Hashids},使用默认{@link #DEFAULT_ALPHABET}作为字母表,不限制最小长度 + * + * @param salt 加盐值 + * @return {@code Hashids} + */ + public static Hashids create(final char[] salt) { + return create(salt, DEFAULT_ALPHABET, -1); + } + + /** + * 根据参数值,创建{@code Hashids},使用默认{@link #DEFAULT_ALPHABET}作为字母表 + * + * @param salt 加盐值 + * @param minLength 限制最小长度,-1表示不限制 + * @return {@code Hashids} + */ + public static Hashids create(final char[] salt, final int minLength) { + return create(salt, DEFAULT_ALPHABET, minLength); + } + + /** + * 根据参数值,创建{@code Hashids} + * + * @param salt 加盐值 + * @param alphabet hash字母表 + * @param minLength 限制最小长度,-1表示不限制 + * @return {@code Hashids} + */ + public static Hashids create(final char[] salt, final char[] alphabet, final int minLength) { + return new Hashids(salt, alphabet, minLength); + } + // endregion + + /** + * 构造 + * + * @param salt 加盐值 + * @param alphabet hash字母表 + * @param minLength 限制最小长度,-1表示不限制 + */ + public Hashids(final char[] salt, final char[] alphabet, final int minLength) { + this.minLength = minLength; + this.salt = Arrays.copyOf(salt, salt.length); + + // filter and shuffle separators + char[] tmpSeparators = shuffle(filterSeparators(DEFAULT_SEPARATORS, alphabet), this.salt); + + // validate and filter the alphabet + char[] tmpAlphabet = validateAndFilterAlphabet(alphabet, tmpSeparators); + + // check separator threshold + if (tmpSeparators.length == 0 || + ((double) (tmpAlphabet.length / tmpSeparators.length)) > SEPARATOR_THRESHOLD) { + final int minSeparatorsSize = (int) Math.ceil(tmpAlphabet.length / SEPARATOR_THRESHOLD); + // check minimum size of separators + if (minSeparatorsSize > tmpSeparators.length) { + // fill separators from alphabet + final int missingSeparators = minSeparatorsSize - tmpSeparators.length; + tmpSeparators = Arrays.copyOf(tmpSeparators, tmpSeparators.length + missingSeparators); + System.arraycopy(tmpAlphabet, 0, tmpSeparators, + tmpSeparators.length - missingSeparators, missingSeparators); + System.arraycopy(tmpAlphabet, 0, tmpSeparators, + tmpSeparators.length - missingSeparators, missingSeparators); + tmpAlphabet = Arrays.copyOfRange(tmpAlphabet, missingSeparators, tmpAlphabet.length); + } + } + + // shuffle the current alphabet + shuffle(tmpAlphabet, this.salt); + + // check guards + this.guards = new char[(int) Math.ceil(tmpAlphabet.length / GUARD_THRESHOLD)]; + if (alphabet.length < 3) { + System.arraycopy(tmpSeparators, 0, guards, 0, guards.length); + this.separators = Arrays.copyOfRange(tmpSeparators, guards.length, tmpSeparators.length); + this.alphabet = tmpAlphabet; + } else { + System.arraycopy(tmpAlphabet, 0, guards, 0, guards.length); + this.separators = tmpSeparators; + this.alphabet = Arrays.copyOfRange(tmpAlphabet, guards.length, tmpAlphabet.length); + } + + // create the separators set + separatorsSet = IntStream.range(0, separators.length) + .mapToObj(idx -> separators[idx]) + .collect(Collectors.toSet()); + } + + /** + * 编码给定的16进制数字 + * + * @param hexNumbers 16进制数字 + * @return 编码后的值, {@code null} if {@code numbers} 是 {@code null}. + * @throws IllegalArgumentException 数字不支持抛出此异常 + */ + public String encodeFromHex(final String hexNumbers) { + if (hexNumbers == null) { + return null; + } + + // remove the prefix, if present + final String hex = hexNumbers.startsWith("0x") || hexNumbers.startsWith("0X") ? + hexNumbers.substring(2) : hexNumbers; + + // get the associated long value and encode it + LongStream values = LongStream.empty(); + final Matcher matcher = HEX_VALUES_PATTERN.matcher(hex); + while (matcher.find()) { + final long value = new BigInteger("1" + matcher.group(), 16).longValue(); + values = LongStream.concat(values, LongStream.of(value)); + } + + return encode(values.toArray()); + } + + /** + * 编码给定的数字数组 + * + * @param numbers 数字数组 + * @return 编码后的值, {@code null} if {@code numbers} 是 {@code null}. + * @throws IllegalArgumentException 数字不支持抛出此异常 + */ + @Override + public String encode(final long... numbers) { + if (numbers == null) { + return null; + } + + // copy alphabet + final char[] currentAlphabet = Arrays.copyOf(alphabet, alphabet.length); + + // determine the lottery number + final long lotteryId = LongStream.range(0, numbers.length) + .reduce(0, (state, i) -> { + final long number = numbers[(int) i]; + if (number < 0) { + throw new IllegalArgumentException("invalid number: " + number); + } + return state + number % (i + LOTTERY_MOD); + }); + final char lottery = currentAlphabet[(int) (lotteryId % currentAlphabet.length)]; + + // encode each number + final StringBuilder global = new StringBuilder(); + IntStream.range(0, numbers.length) + .forEach(idx -> { + // derive alphabet + deriveNewAlphabet(currentAlphabet, salt, lottery); + + // encode + final int initialLength = global.length(); + translate(numbers[idx], currentAlphabet, global, initialLength); + + // prepend the lottery + if (idx == 0) { + global.insert(0, lottery); + } + + // append the separator, if more numbers are pending encoding + if (idx + 1 < numbers.length) { + long n = numbers[idx] % (global.charAt(initialLength) + 1); + global.append(separators[(int) (n % separators.length)]); + } + }); + + // add the guards, if there's any space left + if (minLength > global.length()) { + int guardIdx = (int) ((lotteryId + lottery) % guards.length); + global.insert(0, guards[guardIdx]); + if (minLength > global.length()) { + guardIdx = (int) ((lotteryId + global.charAt(2)) % guards.length); + global.append(guards[guardIdx]); + } + } + + // add the necessary padding + int paddingLeft = minLength - global.length(); + while (paddingLeft > 0) { + shuffle(currentAlphabet, Arrays.copyOf(currentAlphabet, currentAlphabet.length)); + + final int alphabetHalfSize = currentAlphabet.length / 2; + final int initialSize = global.length(); + if (paddingLeft > currentAlphabet.length) { + // entire alphabet with the current encoding in the middle of it + int offset = alphabetHalfSize + (currentAlphabet.length % 2 == 0 ? 0 : 1); + + global.insert(0, currentAlphabet, alphabetHalfSize, offset); + global.insert(offset + initialSize, currentAlphabet, 0, alphabetHalfSize); + // decrease the padding left + paddingLeft -= currentAlphabet.length; + } else { + // calculate the excess + final int excess = currentAlphabet.length + global.length() - minLength; + final int secondHalfStartOffset = alphabetHalfSize + Math.floorDiv(excess, 2); + final int secondHalfLength = currentAlphabet.length - secondHalfStartOffset; + final int firstHalfLength = paddingLeft - secondHalfLength; + + global.insert(0, currentAlphabet, secondHalfStartOffset, secondHalfLength); + global.insert(secondHalfLength + initialSize, currentAlphabet, 0, firstHalfLength); + + paddingLeft = 0; + } + } + + return global.toString(); + } + + //------------------------- + // Decode + //------------------------- + + /** + * 解码Hash值为16进制数字 + * + * @param hash hash值 + * @return 解码后的16进制值, {@code null} if {@code numbers} 是 {@code null}. + * @throws IllegalArgumentException if the hash is invalid. + */ + public String decodeToHex(final String hash) { + if (hash == null) { + return null; + } + + final StringBuilder sb = new StringBuilder(); + Arrays.stream(decode(hash)) + .mapToObj(Long::toHexString) + .forEach(hex -> sb.append(hex, 1, hex.length())); + return sb.toString(); + } + + /** + * 解码Hash值为数字数组 + * + * @param hash hash值 + * @return 解码后的16进制值, {@code null} if {@code numbers} 是 {@code null}. + * @throws IllegalArgumentException if the hash is invalid. + */ + @Override + public long[] decode(final String hash) { + if (hash == null) { + return null; + } + + // create a set of the guards + final Set guardsSet = IntStream.range(0, guards.length) + .mapToObj(idx -> guards[idx]) + .collect(Collectors.toSet()); + // count the total guards used + final int[] guardsIdx = IntStream.range(0, hash.length()) + .filter(idx -> guardsSet.contains(hash.charAt(idx))) + .toArray(); + // get the start/end index base on the guards count + final int startIdx, endIdx; + if (guardsIdx.length > 0) { + startIdx = guardsIdx[0] + 1; + endIdx = guardsIdx.length > 1 ? guardsIdx[1] : hash.length(); + } else { + startIdx = 0; + endIdx = hash.length(); + } + + LongStream decoded = LongStream.empty(); + // parse the hash + if (hash.length() > 0) { + final char lottery = hash.charAt(startIdx); + + // create the initial accumulation string + final int length = hash.length() - guardsIdx.length - 1; + StringBuilder block = new StringBuilder(length); + + // create the base salt + final char[] decodeSalt = new char[alphabet.length]; + decodeSalt[0] = lottery; + final int saltLength = salt.length >= alphabet.length ? alphabet.length - 1 : salt.length; + System.arraycopy(salt, 0, decodeSalt, 1, saltLength); + final int saltLeft = alphabet.length - saltLength - 1; + + // copy alphabet + final char[] currentAlphabet = Arrays.copyOf(alphabet, alphabet.length); + + for (int i = startIdx + 1; i < endIdx; i++) { + if (!separatorsSet.contains(hash.charAt(i))) { + block.append(hash.charAt(i)); + // continue if we have not reached the end, yet + if (i < endIdx - 1) { + continue; + } + } + + if (block.length() > 0) { + // create the salt + if (saltLeft > 0) { + System.arraycopy(currentAlphabet, 0, decodeSalt, + alphabet.length - saltLeft, saltLeft); + } + + // shuffle the alphabet + shuffle(currentAlphabet, decodeSalt); + + // prepend the decoded value + final long n = translate(block.toString().toCharArray(), currentAlphabet); + decoded = LongStream.concat(decoded, LongStream.of(n)); + + // create a new block + block = new StringBuilder(length); + } + } + } + + // validate the hash + final long[] decodedValue = decoded.toArray(); + if (!Objects.equals(hash, encode(decodedValue))) { + throw new IllegalArgumentException("invalid hash: " + hash); + } + + return decodedValue; + } + + private StringBuilder translate(final long n, final char[] alphabet, + final StringBuilder sb, final int start) { + long input = n; + do { + // prepend the chosen char + sb.insert(start, alphabet[(int) (input % alphabet.length)]); + + // trim the input + input = input / alphabet.length; + } while (input > 0); + + return sb; + } + + private long translate(final char[] hash, final char[] alphabet) { + long number = 0; + + final Map alphabetMapping = IntStream.range(0, alphabet.length) + .mapToObj(idx -> new Object[]{alphabet[idx], idx}) + .collect(Collectors.groupingBy(arr -> (Character) arr[0], + Collectors.mapping(arr -> (Integer) arr[1], + Collectors.reducing(null, (a, b) -> a == null ? b : a)))); + + for (int i = 0; i < hash.length; ++i) { + number += alphabetMapping.computeIfAbsent(hash[i], k -> { + throw new IllegalArgumentException("Invalid alphabet for hash"); + }) * (long) Math.pow(alphabet.length, hash.length - i - 1); + } + + return number; + } + + private char[] deriveNewAlphabet(final char[] alphabet, final char[] salt, final char lottery) { + // create the new salt + final char[] newSalt = new char[alphabet.length]; + + // 1. lottery + newSalt[0] = lottery; + int spaceLeft = newSalt.length - 1; + int offset = 1; + // 2. salt + if (salt.length > 0 && spaceLeft > 0) { + int length = Math.min(salt.length, spaceLeft); + System.arraycopy(salt, 0, newSalt, offset, length); + spaceLeft -= length; + offset += length; + } + // 3. alphabet + if (spaceLeft > 0) { + System.arraycopy(alphabet, 0, newSalt, offset, spaceLeft); + } + + // shuffle + return shuffle(alphabet, newSalt); + } + + private char[] validateAndFilterAlphabet(final char[] alphabet, final char[] separators) { + // validate size + if (alphabet.length < MIN_ALPHABET_LENGTH) { + throw new IllegalArgumentException(String.format("alphabet must contain at least %d unique " + + "characters: %d", MIN_ALPHABET_LENGTH, alphabet.length)); + } + + final Set seen = new LinkedHashSet<>(alphabet.length); + final Set invalid = IntStream.range(0, separators.length) + .mapToObj(idx -> separators[idx]) + .collect(Collectors.toSet()); + + // add to seen set (without duplicates) + IntStream.range(0, alphabet.length) + .forEach(i -> { + if (alphabet[i] == ' ') { + throw new IllegalArgumentException(String.format("alphabet must not contain spaces: " + + "index %d", i)); + } + final Character c = alphabet[i]; + if (!invalid.contains(c)) { + seen.add(c); + } + }); + + // create a new alphabet without the duplicates + final char[] uniqueAlphabet = new char[seen.size()]; + int idx = 0; + for (char c : seen) { + uniqueAlphabet[idx++] = c; + } + return uniqueAlphabet; + } + + @SuppressWarnings("SameParameterValue") + private char[] filterSeparators(final char[] separators, final char[] alphabet) { + final Set valid = IntStream.range(0, alphabet.length) + .mapToObj(idx -> alphabet[idx]) + .collect(Collectors.toSet()); + + return IntStream.range(0, separators.length) + .mapToObj(idx -> (separators[idx])) + .filter(valid::contains) + // ugly way to convert back to char[] + .map(c -> Character.toString(c)) + .collect(Collectors.joining()) + .toCharArray(); + } + + private char[] shuffle(final char[] alphabet, final char[] salt) { + for (int i = alphabet.length - 1, v = 0, p = 0, j, z; salt.length > 0 && i > 0; i--, v++) { + v %= salt.length; + p += z = salt[v]; + j = (z + v + p) % i; + final char tmp = alphabet[j]; + alphabet[j] = alphabet[i]; + alphabet[i] = tmp; + } + return alphabet; + } +} diff --git a/src/main/java/cn/hutool/core/codec/Morse.java b/src/main/java/cn/hutool/core/codec/Morse.java new file mode 100644 index 0000000..2fdb04e --- /dev/null +++ b/src/main/java/cn/hutool/core/codec/Morse.java @@ -0,0 +1,172 @@ +package cn.hutool.core.codec; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.CharUtil; +import cn.hutool.core.util.StrUtil; + +/** + * 莫尔斯电码的编码和解码实现
+ * 参考:https://github.com/TakWolf/Java-MorseCoder + * + * @author looly, TakWolf + * @since 4.4.1 + */ +public class Morse { + + private static final Map ALPHABETS = new HashMap<>(); // code point -> morse + private static final Map DICTIONARIES = new HashMap<>(); // morse -> code point + + /** + * 注册莫尔斯电码表 + * + * @param abc 字母和字符 + * @param dict 二进制 + */ + private static void registerMorse(Character abc, String dict) { + ALPHABETS.put((int) abc, dict); + DICTIONARIES.put(dict, (int) abc); + } + + static { + // Letters + registerMorse('A', "01"); + registerMorse('B', "1000"); + registerMorse('C', "1010"); + registerMorse('D', "100"); + registerMorse('E', "0"); + registerMorse('F', "0010"); + registerMorse('G', "110"); + registerMorse('H', "0000"); + registerMorse('I', "00"); + registerMorse('J', "0111"); + registerMorse('K', "101"); + registerMorse('L', "0100"); + registerMorse('M', "11"); + registerMorse('N', "10"); + registerMorse('O', "111"); + registerMorse('P', "0110"); + registerMorse('Q', "1101"); + registerMorse('R', "010"); + registerMorse('S', "000"); + registerMorse('T', "1"); + registerMorse('U', "001"); + registerMorse('V', "0001"); + registerMorse('W', "011"); + registerMorse('X', "1001"); + registerMorse('Y', "1011"); + registerMorse('Z', "1100"); + // Numbers + registerMorse('0', "11111"); + registerMorse('1', "01111"); + registerMorse('2', "00111"); + registerMorse('3', "00011"); + registerMorse('4', "00001"); + registerMorse('5', "00000"); + registerMorse('6', "10000"); + registerMorse('7', "11000"); + registerMorse('8', "11100"); + registerMorse('9', "11110"); + // Punctuation + registerMorse('.', "010101"); + registerMorse(',', "110011"); + registerMorse('?', "001100"); + registerMorse('\'', "011110"); + registerMorse('!', "101011"); + registerMorse('/', "10010"); + registerMorse('(', "10110"); + registerMorse(')', "101101"); + registerMorse('&', "01000"); + registerMorse(':', "111000"); + registerMorse(';', "101010"); + registerMorse('=', "10001"); + registerMorse('+', "01010"); + registerMorse('-', "100001"); + registerMorse('_', "001101"); + registerMorse('"', "010010"); + registerMorse('$', "0001001"); + registerMorse('@', "011010"); + } + + private final char dit; // short mark or dot + private final char dah; // longer mark or dash + private final char split; + + /** + * 构造 + */ + public Morse() { + this(CharUtil.DOT, CharUtil.DASHED, CharUtil.SLASH); + } + + /** + * 构造 + * + * @param dit 点表示的字符 + * @param dah 横线表示的字符 + * @param split 分隔符 + */ + public Morse(char dit, char dah, char split) { + this.dit = dit; + this.dah = dah; + this.split = split; + } + + /** + * 编码 + * + * @param text 文本 + * @return 密文 + */ + public String encode(String text) { + Assert.notNull(text, "Text should not be null."); + + text = text.toUpperCase(); + final StringBuilder morseBuilder = new StringBuilder(); + final int len = text.codePointCount(0, text.length()); + for (int i = 0; i < len; i++) { + int codePoint = text.codePointAt(i); + String word = ALPHABETS.get(codePoint); + if (word == null) { + word = Integer.toBinaryString(codePoint); + } + morseBuilder.append(word.replace('0', dit).replace('1', dah)).append(split); + } + return morseBuilder.toString(); + } + + /** + * 解码 + * + * @param morse 莫尔斯电码 + * @return 明文 + */ + public String decode(String morse) { + Assert.notNull(morse, "Morse should not be null."); + + final char dit = this.dit; + final char dah = this.dah; + final char split = this.split; + if (!StrUtil.containsOnly(morse, dit, dah, split)) { + throw new IllegalArgumentException("Incorrect morse."); + } + final List words = StrUtil.split(morse, split); + final StringBuilder textBuilder = new StringBuilder(); + Integer codePoint; + for (String word : words) { + if(StrUtil.isEmpty(word)){ + continue; + } + word = word.replace(dit, '0').replace(dah, '1'); + codePoint = DICTIONARIES.get(word); + if (codePoint == null) { + codePoint = Integer.valueOf(word, 2); + } + textBuilder.appendCodePoint(codePoint); + } + return textBuilder.toString(); + } +} diff --git a/src/main/java/cn/hutool/core/codec/PercentCodec.java b/src/main/java/cn/hutool/core/codec/PercentCodec.java new file mode 100644 index 0000000..9d91cd1 --- /dev/null +++ b/src/main/java/cn/hutool/core/codec/PercentCodec.java @@ -0,0 +1,198 @@ +package cn.hutool.core.codec; + +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.CharUtil; +import cn.hutool.core.util.HexUtil; +import cn.hutool.core.util.StrUtil; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.io.Serializable; +import java.nio.charset.Charset; +import java.util.BitSet; + +/** + * 百分号编码(Percent-encoding), 也称作URL编码(URL encoding)。
+ * 百分号编码可用于URI的编码,也可以用于"application/x-www-form-urlencoded"的MIME准备数据。 + * + *

+ * 百分号编码会对 URI 中不允许出现的字符或者其他特殊情况的允许的字符进行编码,对于被编码的字符,最终会转为以百分号"%“开头,后面跟着两位16进制数值的形式。 + * 举个例子,空格符(SP)是不允许的字符,在 ASCII 码对应的二进制值是"00100000”,最终转为"%20"。 + *

+ *

+ * 对于不同场景应遵循不同规范: + * + *

    + *
  • URI:遵循RFC 3986保留字规范
  • + *
  • application/x-www-form-urlencoded,遵循W3C HTML Form content types规范,如空格须转+
  • + *
+ * + * @author looly + * @since 5.7.16 + */ +public class PercentCodec implements Serializable { + private static final long serialVersionUID = 1L; + + /** + * 从已知PercentCodec创建PercentCodec,会复制给定PercentCodec的安全字符 + * + * @param codec PercentCodec + * @return PercentCodec + */ + public static PercentCodec of(PercentCodec codec) { + return new PercentCodec((BitSet) codec.safeCharacters.clone()); + } + + /** + * 创建PercentCodec,使用指定字符串中的字符作为安全字符 + * + * @param chars 安全字符合集 + * @return PercentCodec + */ + public static PercentCodec of(CharSequence chars) { + Assert.notNull(chars, "chars must not be null"); + final PercentCodec codec = new PercentCodec(); + final int length = chars.length(); + for (int i = 0; i < length; i++) { + codec.addSafe(chars.charAt(i)); + } + return codec; + } + + /** + * 存放安全编码 + */ + private final BitSet safeCharacters; + + /** + * 是否编码空格为+
+ * 如果为{@code true},则将空格编码为"+",此项只在"application/x-www-form-urlencoded"中使用
+ * 如果为{@code false},则空格编码为"%20",此项一般用于URL的Query部分(RFC3986规范) + */ + private boolean encodeSpaceAsPlus = false; + + /** + * 构造
+ * [a-zA-Z0-9]默认不被编码 + */ + public PercentCodec() { + this(new BitSet(256)); + } + + /** + * 构造 + * + * @param safeCharacters 安全字符,安全字符不被编码 + */ + public PercentCodec(BitSet safeCharacters) { + this.safeCharacters = safeCharacters; + } + + /** + * 增加安全字符
+ * 安全字符不被编码 + * + * @param c 字符 + * @return this + */ + public PercentCodec addSafe(char c) { + safeCharacters.set(c); + return this; + } + + /** + * 移除安全字符
+ * 安全字符不被编码 + * + * @param c 字符 + * @return this + */ + public PercentCodec removeSafe(char c) { + safeCharacters.clear(c); + return this; + } + + /** + * 增加安全字符到挡墙的PercentCodec + * + * @param codec PercentCodec + * @return this + */ + public PercentCodec or(PercentCodec codec) { + this.safeCharacters.or(codec.safeCharacters); + return this; + } + + /** + * 组合当前PercentCodec和指定PercentCodec为一个新的PercentCodec,安全字符为并集 + * + * @param codec PercentCodec + * @return 新的PercentCodec + */ + public PercentCodec orNew(PercentCodec codec) { + return of(this).or(codec); + } + + /** + * 是否将空格编码为+
+ * 如果为{@code true},则将空格编码为"+",此项只在"application/x-www-form-urlencoded"中使用
+ * 如果为{@code false},则空格编码为"%20",此项一般用于URL的Query部分(RFC3986规范) + * + * @param encodeSpaceAsPlus 是否将空格编码为+ + * @return this + */ + public PercentCodec setEncodeSpaceAsPlus(boolean encodeSpaceAsPlus) { + this.encodeSpaceAsPlus = encodeSpaceAsPlus; + return this; + } + + /** + * 将URL中的字符串编码为%形式 + * + * @param path 需要编码的字符串 + * @param charset 编码, {@code null}返回原字符串,表示不编码 + * @param customSafeChar 自定义安全字符 + * @return 编码后的字符串 + */ + public String encode(CharSequence path, Charset charset, char... customSafeChar) { + if (null == charset || StrUtil.isEmpty(path)) { + return StrUtil.str(path); + } + + final StringBuilder rewrittenPath = new StringBuilder(path.length()); + final ByteArrayOutputStream buf = new ByteArrayOutputStream(); + final OutputStreamWriter writer = new OutputStreamWriter(buf, charset); + + char c; + for (int i = 0; i < path.length(); i++) { + c = path.charAt(i); + if (safeCharacters.get(c) || ArrayUtil.contains(customSafeChar, c)) { + rewrittenPath.append(c); + } else if (encodeSpaceAsPlus && c == CharUtil.SPACE) { + // 对于空格单独处理 + rewrittenPath.append('+'); + } else { + // convert to external encoding before hex conversion + try { + writer.write(c); + writer.flush(); + } catch (IOException e) { + buf.reset(); + continue; + } + + // 兼容双字节的Unicode符处理(如部分emoji) + byte[] ba = buf.toByteArray(); + for (byte toEncode : ba) { + // Converting each byte in the buffer + rewrittenPath.append('%'); + HexUtil.appendHex(rewrittenPath, toEncode, false); + } + buf.reset(); + } + } + return rewrittenPath.toString(); + } +} diff --git a/src/main/java/cn/hutool/core/codec/PunyCode.java b/src/main/java/cn/hutool/core/codec/PunyCode.java new file mode 100644 index 0000000..ea3b32d --- /dev/null +++ b/src/main/java/cn/hutool/core/codec/PunyCode.java @@ -0,0 +1,313 @@ +package cn.hutool.core.codec; + +import cn.hutool.core.exceptions.UtilException; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.CharUtil; +import cn.hutool.core.util.StrUtil; + +import java.util.List; + +/** + * Punycode是一个根据RFC 3492标准而制定的编码系统,主要用于把域名从地方语言所采用的Unicode编码转换成为可用于DNS系统的编码 + *

+ * 参考:https://blog.csdn.net/a19881029/article/details/18262671 + * + * @author looly + * @since 5.5.2 + */ +public class PunyCode { + private static final int TMIN = 1; + private static final int TMAX = 26; + private static final int BASE = 36; + private static final int INITIAL_N = 128; + private static final int INITIAL_BIAS = 72; + private static final int DAMP = 700; + private static final int SKEW = 38; + private static final char DELIMITER = '-'; + + public static final String PUNY_CODE_PREFIX = "xn--"; + + /** + * punycode转码域名 + * + * @param domain 域名 + * @return 编码后的域名 + * @throws UtilException 计算异常 + */ + public static String encodeDomain(String domain) throws UtilException { + Assert.notNull(domain, "domain must not be null!"); + final List split = StrUtil.split(domain, CharUtil.DOT); + final StringBuilder result = new StringBuilder(domain.length() * 4); + for (final String str : split) { + if (result.length() != 0) { + result.append(CharUtil.DOT); + } + result.append(encode(str, true)); + } + + return result.toString(); + } + + /** + * 将内容编码为PunyCode + * + * @param input 字符串 + * @return PunyCode字符串 + * @throws UtilException 计算异常 + */ + public static String encode(CharSequence input) throws UtilException { + return encode(input, false); + } + + /** + * 将内容编码为PunyCode + * + * @param input 字符串 + * @param withPrefix 是否包含 "xn--"前缀 + * @return PunyCode字符串 + * @throws UtilException 计算异常 + */ + public static String encode(CharSequence input, boolean withPrefix) throws UtilException { + Assert.notNull(input, "input must not be null!"); + int n = INITIAL_N; + int delta = 0; + int bias = INITIAL_BIAS; + StringBuilder output = new StringBuilder(); + // Copy all basic code points to the output + final int length = input.length(); + int b = 0; + for (int i = 0; i < length; i++) { + char c = input.charAt(i); + if (isBasic(c)) { + output.append(c); + b++; + } + } + // Append delimiter + if (b > 0) { + if(b == length){ + // 无需要编码的字符 + return output.toString(); + } + output.append(DELIMITER); + } + int h = b; + while (h < length) { + int m = Integer.MAX_VALUE; + // Find the minimum code point >= n + for (int i = 0; i < length; i++) { + final char c = input.charAt(i); + if (c >= n && c < m) { + m = c; + } + } + if (m - n > (Integer.MAX_VALUE - delta) / (h + 1)) { + throw new UtilException("OVERFLOW"); + } + delta = delta + (m - n) * (h + 1); + n = m; + for (int j = 0; j < length; j++) { + int c = input.charAt(j); + if (c < n) { + delta++; + if (0 == delta) { + throw new UtilException("OVERFLOW"); + } + } + if (c == n) { + int q = delta; + for (int k = BASE; ; k += BASE) { + int t; + if (k <= bias) { + t = TMIN; + } else if (k >= bias + TMAX) { + t = TMAX; + } else { + t = k - bias; + } + if (q < t) { + break; + } + output.append((char) digit2codepoint(t + (q - t) % (BASE - t))); + q = (q - t) / (BASE - t); + } + output.append((char) digit2codepoint(q)); + bias = adapt(delta, h + 1, h == b); + delta = 0; + h++; + } + } + delta++; + n++; + } + + if (withPrefix) { + output.insert(0, PUNY_CODE_PREFIX); + } + return output.toString(); + } + + /** + * 解码punycode域名 + * + * @param domain PunyCode域名 + * @return 解码后的域名 + * @throws UtilException 计算异常 + */ + public static String decodeDomain(String domain) throws UtilException { + Assert.notNull(domain, "domain must not be null!"); + final List split = StrUtil.split(domain, CharUtil.DOT); + final StringBuilder result = new StringBuilder(domain.length() / 4 + 1); + for (final String str : split) { + if (result.length() != 0) { + result.append(CharUtil.DOT); + } + result.append(StrUtil.startWithIgnoreEquals(str, PUNY_CODE_PREFIX) ? decode(str) : str); + } + + return result.toString(); + } + + /** + * 解码 PunyCode为字符串 + * + * @param input PunyCode + * @return 字符串 + * @throws UtilException 计算异常 + */ + public static String decode(String input) throws UtilException { + Assert.notNull(input, "input must not be null!"); + input = StrUtil.removePrefixIgnoreCase(input, PUNY_CODE_PREFIX); + + int n = INITIAL_N; + int i = 0; + int bias = INITIAL_BIAS; + StringBuilder output = new StringBuilder(); + int d = input.lastIndexOf(DELIMITER); + if (d > 0) { + for (int j = 0; j < d; j++) { + final char c = input.charAt(j); + if (isBasic(c)) { + output.append(c); + } + } + d++; + } else { + d = 0; + } + final int length = input.length(); + while (d < length) { + int oldi = i; + int w = 1; + for (int k = BASE; ; k += BASE) { + if (d == length) { + throw new UtilException("BAD_INPUT"); + } + int c = input.charAt(d++); + int digit = codepoint2digit(c); + if (digit > (Integer.MAX_VALUE - i) / w) { + throw new UtilException("OVERFLOW"); + } + i = i + digit * w; + int t; + if (k <= bias) { + t = TMIN; + } else if (k >= bias + TMAX) { + t = TMAX; + } else { + t = k - bias; + } + if (digit < t) { + break; + } + w = w * (BASE - t); + } + bias = adapt(i - oldi, output.length() + 1, oldi == 0); + if (i / (output.length() + 1) > Integer.MAX_VALUE - n) { + throw new UtilException("OVERFLOW"); + } + n = n + i / (output.length() + 1); + i = i % (output.length() + 1); + output.insert(i, (char) n); + i++; + } + + return output.toString(); + } + + private static int adapt(int delta, int numpoints, boolean first) { + if (first) { + delta = delta / DAMP; + } else { + delta = delta / 2; + } + delta = delta + (delta / numpoints); + int k = 0; + while (delta > ((BASE - TMIN) * TMAX) / 2) { + delta = delta / (BASE - TMIN); + k = k + BASE; + } + return k + ((BASE - TMIN + 1) * delta) / (delta + SKEW); + } + + private static boolean isBasic(char c) { + return c < 0x80; + } + + /** + * 将数字转为字符,对应关系为: + *

+	 *     0 -> a
+	 *     1 -> b
+	 *     ...
+	 *     25 -> z
+	 *     26 -> '0'
+	 *     ...
+	 *     35 -> '9'
+	 * 
+ * + * @param d 输入字符 + * @return 转换后的字符 + * @throws UtilException 无效字符 + */ + private static int digit2codepoint(int d) throws UtilException { + Assert.checkBetween(d, 0, 35); + if (d < 26) { + // 0..25 : 'a'..'z' + return d + 'a'; + } else if (d < 36) { + // 26..35 : '0'..'9'; + return d - 26 + '0'; + } else { + throw new UtilException("BAD_INPUT"); + } + } + + /** + * 将字符转为数字,对应关系为: + *
+	 *     a -> 0
+	 *     b -> 1
+	 *     ...
+	 *     z -> 25
+	 *     '0' -> 26
+	 *     ...
+	 *     '9' -> 35
+	 * 
+ * + * @param c 输入字符 + * @return 转换后的字符 + * @throws UtilException 无效字符 + */ + private static int codepoint2digit(int c) throws UtilException { + if (c - '0' < 10) { + // '0'..'9' : 26..35 + return c - '0' + 26; + } else if (c - 'a' < 26) { + // 'a'..'z' : 0..25 + return c - 'a'; + } else { + throw new UtilException("BAD_INPUT"); + } + } +} diff --git a/src/main/java/cn/hutool/core/codec/Rot.java b/src/main/java/cn/hutool/core/codec/Rot.java new file mode 100644 index 0000000..d0826ae --- /dev/null +++ b/src/main/java/cn/hutool/core/codec/Rot.java @@ -0,0 +1,177 @@ +package cn.hutool.core.codec; + +import cn.hutool.core.lang.Assert; + +/** + * RotN(rotate by N places),回转N位密码,是一种简易的替换式密码,也是过去在古罗马开发的凯撒加密的一种变体。
+ * 代码来自:https://github.com/orclight/jencrypt + * + * @author looly,shuzhilong + * @since 4.4.1 + */ +public class Rot { + + private static final char aCHAR = 'a'; + private static final char zCHAR = 'z'; + private static final char ACHAR = 'A'; + private static final char ZCHAR = 'Z'; + private static final char CHAR0 = '0'; + private static final char CHAR9 = '9'; + + /** + * Rot-13编码,同时编码数字 + * + * @param message 被编码的消息 + * @return 编码后的字符串 + */ + public static String encode13(String message) { + return encode13(message, true); + } + + /** + * Rot-13编码 + * + * @param message 被编码的消息 + * @param isEncodeNumber 是否编码数字 + * @return 编码后的字符串 + */ + public static String encode13(String message, boolean isEncodeNumber) { + return encode(message, 13, isEncodeNumber); + } + + /** + * RotN编码 + * + * @param message 被编码的消息 + * @param offset 位移,常用位移13 + * @param isEncodeNumber 是否编码数字 + * @return 编码后的字符串 + */ + public static String encode(String message, int offset, boolean isEncodeNumber) { + Assert.notNull(message, "message must not be null"); + final int len = message.length(); + final char[] chars = new char[len]; + + for (int i = 0; i < len; i++) { + chars[i] = encodeChar(message.charAt(i), offset, isEncodeNumber); + } + return new String(chars); + } + + /** + * Rot-13解码,同时解码数字 + * + * @param rot 被解码的消息密文 + * @return 解码后的字符串 + */ + public static String decode13(String rot) { + return decode13(rot, true); + } + + /** + * Rot-13解码 + * + * @param rot 被解码的消息密文 + * @param isDecodeNumber 是否解码数字 + * @return 解码后的字符串 + */ + public static String decode13(String rot, boolean isDecodeNumber) { + return decode(rot, 13, isDecodeNumber); + } + + /** + * RotN解码 + * + * @param rot 被解码的消息密文 + * @param offset 位移,常用位移13 + * @param isDecodeNumber 是否解码数字 + * @return 解码后的字符串 + */ + public static String decode(String rot, int offset, boolean isDecodeNumber) { + Assert.notNull(rot, "rot must not be null"); + final int len = rot.length(); + final char[] chars = new char[len]; + + for (int i = 0; i < len; i++) { + chars[i] = decodeChar(rot.charAt(i), offset, isDecodeNumber); + } + return new String(chars); + } + + // ------------------------------------------------------------------------------------------ Private method start + /** + * 解码字符 + * + * @param c 字符 + * @param offset 位移 + * @param isDecodeNumber 是否解码数字 + * @return 解码后的字符串 + */ + private static char encodeChar(char c, int offset, boolean isDecodeNumber) { + if (isDecodeNumber) { + if (c >= CHAR0 && c <= CHAR9) { + c -= CHAR0; + c = (char) ((c + offset) % 10); + c += CHAR0; + } + } + + // A == 65, Z == 90 + if (c >= ACHAR && c <= ZCHAR) { + c -= ACHAR; + c = (char) ((c + offset) % 26); + c += ACHAR; + } + // a == 97, z == 122. + else if (c >= aCHAR && c <= zCHAR) { + c -= aCHAR; + c = (char) ((c + offset) % 26); + c += aCHAR; + } + return c; + } + + /** + * 编码字符 + * + * @param c 字符 + * @param offset 位移 + * @param isDecodeNumber 是否编码数字 + * @return 编码后的字符串 + */ + private static char decodeChar(char c, int offset, boolean isDecodeNumber) { + int temp = c; + // if converting numbers is enabled + if (isDecodeNumber) { + if (temp >= CHAR0 && temp <= CHAR9) { + temp -= CHAR0; + temp = temp - offset; + while (temp < 0) { + temp += 10; + } + temp += CHAR0; + } + } + + // A == 65, Z == 90 + if (temp >= ACHAR && temp <= ZCHAR) { + temp -= ACHAR; + + temp = temp - offset; + while (temp < 0) { + temp = 26 + temp; + } + temp += ACHAR; + } else if (temp >= aCHAR && temp <= zCHAR) { + temp -= aCHAR; + + temp = temp - offset; + if (temp < 0) + temp = 26 + temp; + + temp += aCHAR; + } + return (char) temp; + } + // ------------------------------------------------------------------------------------------ Private method end +} diff --git a/src/main/java/cn/hutool/core/codec/package-info.java b/src/main/java/cn/hutool/core/codec/package-info.java new file mode 100644 index 0000000..16774ac --- /dev/null +++ b/src/main/java/cn/hutool/core/codec/package-info.java @@ -0,0 +1,7 @@ +/** + * BaseN以及BCD编码封装 + * + * @author looly + * + */ +package cn.hutool.core.codec; \ No newline at end of file diff --git a/src/main/java/cn/hutool/core/collection/ArrayIter.java b/src/main/java/cn/hutool/core/collection/ArrayIter.java new file mode 100644 index 0000000..bfe20ef --- /dev/null +++ b/src/main/java/cn/hutool/core/collection/ArrayIter.java @@ -0,0 +1,133 @@ +package cn.hutool.core.collection; + +import java.io.Serializable; +import java.lang.reflect.Array; +import java.util.NoSuchElementException; + +/** + * 数组Iterator对象 + * + * @param 元素类型 + * @author Looly + * @since 4.1.1 + */ +public class ArrayIter implements IterableIter, ResettableIter, Serializable { + private static final long serialVersionUID = 1L; + + /** + * 数组 + */ + private final Object array; + /** + * 起始位置 + */ + private int startIndex; + /** + * 结束位置 + */ + private int endIndex; + /** + * 当前位置 + */ + private int index; + + /** + * 构造 + * + * @param array 数组 + * @throws IllegalArgumentException array对象不为数组抛出此异常 + * @throws NullPointerException array对象为null + */ + public ArrayIter(E[] array) { + this((Object) array); + } + + /** + * 构造 + * + * @param array 数组 + * @throws IllegalArgumentException array对象不为数组抛出此异常 + * @throws NullPointerException array对象为null + */ + public ArrayIter(Object array) { + this(array, 0); + } + + /** + * 构造 + * + * @param array 数组 + * @param startIndex 起始位置,当起始位置小于0或者大于结束位置,置为0。 + * @throws IllegalArgumentException array对象不为数组抛出此异常 + * @throws NullPointerException array对象为null + */ + public ArrayIter(Object array, int startIndex) { + this(array, startIndex, -1); + } + + /** + * 构造 + * + * @param array 数组 + * @param startIndex 起始位置,当起始位置小于0或者大于结束位置,置为0。 + * @param endIndex 结束位置,当结束位置小于0或者大于数组长度,置为数组长度。 + * @throws IllegalArgumentException array对象不为数组抛出此异常 + * @throws NullPointerException array对象为null + */ + public ArrayIter(final Object array, final int startIndex, final int endIndex) { + this.endIndex = Array.getLength(array); + if (endIndex > 0 && endIndex < this.endIndex) { + this.endIndex = endIndex; + } + + if (startIndex >= 0 && startIndex < this.endIndex) { + this.startIndex = startIndex; + } + this.array = array; + this.index = this.startIndex; + } + + @Override + public boolean hasNext() { + return (index < endIndex); + } + + @Override + @SuppressWarnings("unchecked") + public E next() { + if (!hasNext()) { + throw new NoSuchElementException(); + } + return (E) Array.get(array, index++); + } + + /** + * 不允许操作数组元素 + * + * @throws UnsupportedOperationException always + */ + @Override + public void remove() { + throw new UnsupportedOperationException("remove() method is not supported"); + } + + // Properties + // ----------------------------------------------------------------------- + + /** + * 获得原始数组对象 + * + * @return 原始数组对象 + */ + public Object getArray() { + return array; + } + + /** + * 重置数组位置 + */ + @Override + public void reset() { + this.index = this.startIndex; + } +} diff --git a/src/main/java/cn/hutool/core/collection/AvgPartition.java b/src/main/java/cn/hutool/core/collection/AvgPartition.java new file mode 100644 index 0000000..d7ab3b5 --- /dev/null +++ b/src/main/java/cn/hutool/core/collection/AvgPartition.java @@ -0,0 +1,59 @@ +package cn.hutool.core.collection; + +import cn.hutool.core.lang.Assert; + +import java.util.List; + +/** + * 列表分区或分段
+ * 通过传入分区个数,将指定列表分区为不同的块,每块区域的长度均匀分布(个数差不超过1)
+ *
+ *     [1,2,3,4] -》 [1,2], [3, 4]
+ *     [1,2,3,4] -》 [1,2], [3], [4]
+ *     [1,2,3,4] -》 [1], [2], [3], [4]
+ *     [1,2,3,4] -》 [1], [2], [3], [4], []
+ * 
+ * 分区是在原List的基础上进行的,返回的分区是不可变的抽象列表,原列表元素变更,分区中元素也会变更。 + * + * @param 元素类型 + * @author looly + * @since 5.7.10 + */ +public class AvgPartition extends Partition { + + final int limit; + // 平均分完后剩余的个数,平均放在前remainder个分区中 + final int remainder; + + /** + * 列表分区 + * + * @param list 被分区的列表 + * @param limit 分区个数 + */ + public AvgPartition(List list, int limit) { + super(list, list.size() / (limit <= 0 ? 1 : limit)); + Assert.isTrue(limit > 0, "Partition limit must be > 0"); + this.limit = limit; + this.remainder = list.size() % limit; + } + + @Override + public List get(int index) { + final int size = this.size; + final int remainder = this.remainder; + // 当limit个数超过list的size时,size为0,此时每个分区分1个元素,直到remainder个分配完,剩余分区为[] + int start = index * size + Math.min(index, remainder); + int end = start + size; + if (index + 1 <= remainder) { + // 将remainder个元素平均分布在前面,每个分区分1个 + end += 1; + } + return list.subList(start, end); + } + + @Override + public int size() { + return limit; + } +} diff --git a/src/main/java/cn/hutool/core/collection/BoundedPriorityQueue.java b/src/main/java/cn/hutool/core/collection/BoundedPriorityQueue.java new file mode 100644 index 0000000..7824ce7 --- /dev/null +++ b/src/main/java/cn/hutool/core/collection/BoundedPriorityQueue.java @@ -0,0 +1,90 @@ +package cn.hutool.core.collection; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.Iterator; +import java.util.PriorityQueue; + +/** + * 有界优先队列
+ * 按照给定的排序规则,排序元素,当队列满时,按照给定的排序规则淘汰末尾元素(去除末尾元素) + * @author xiaoleilu + * + * @param 成员类型 + */ +public class BoundedPriorityQueue extends PriorityQueue{ + private static final long serialVersionUID = 3794348988671694820L; + + //容量 + private final int capacity; + private final Comparator comparator; + + public BoundedPriorityQueue(int capacity) { + this(capacity, null); + } + + /** + * 构造 + * @param capacity 容量 + * @param comparator 比较器 + */ + public BoundedPriorityQueue(int capacity, final Comparator comparator) { + super(capacity, (o1, o2) -> { + int cResult; + if(comparator != null) { + cResult = comparator.compare(o1, o2); + }else { + @SuppressWarnings("unchecked") + Comparable o1c = (Comparable)o1; + cResult = o1c.compareTo(o2); + } + + return - cResult; + }); + this.capacity = capacity; + this.comparator = comparator; + } + + /** + * 加入元素,当队列满时,淘汰末尾元素 + * @param e 元素 + * @return 加入成功与否 + */ + @Override + public boolean offer(E e) { + if(size() >= capacity) { + E head = peek(); + if (this.comparator().compare(e, head) <= 0){ + return true; + } + //当队列满时,就要淘汰顶端队列 + poll(); + } + return super.offer(e); + } + + /** + * 添加多个元素
+ * 参数为集合的情况请使用{@link PriorityQueue#addAll} + * @param c 元素数组 + * @return 是否发生改变 + */ + public boolean addAll(E[] c) { + return this.addAll(Arrays.asList(c)); + } + + /** + * @return 返回排序后的列表 + */ + public ArrayList toList() { + final ArrayList list = new ArrayList<>(this); + list.sort(comparator); + return list; + } + + @Override + public Iterator iterator() { + return toList().iterator(); + } +} diff --git a/src/main/java/cn/hutool/core/collection/CollStreamUtil.java b/src/main/java/cn/hutool/core/collection/CollStreamUtil.java new file mode 100644 index 0000000..e99f343 --- /dev/null +++ b/src/main/java/cn/hutool/core/collection/CollStreamUtil.java @@ -0,0 +1,388 @@ +package cn.hutool.core.collection; + + +import cn.hutool.core.lang.Opt; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.stream.CollectorUtil; +import cn.hutool.core.stream.StreamUtil; + +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.stream.Collector; +import java.util.stream.Collectors; + +/** + * 集合的stream操作封装 + * + * @author 528910437@QQ.COM, VampireAchao<achao1441470436@gmail.com>Lion Li> + * @since 5.5.2 + */ +public class CollStreamUtil { + + /** + * 将collection转化为类型不变的map
+ * {@code Collection ----> Map} + * + * @param collection 需要转化的集合 + * @param key V类型转化为K类型的lambda方法 + * @param collection中的泛型 + * @param map中的key类型 + * @return 转化后的map + */ + public static Map toIdentityMap(Collection collection, Function key) { + return toIdentityMap(collection, key, false); + } + + + /** + * 将collection转化为类型不变的map
+ * {@code Collection ----> Map} + * + * @param collection 需要转化的集合 + * @param key V类型转化为K类型的lambda方法 + * @param isParallel 是否并行流 + * @param collection中的泛型 + * @param map中的key类型 + * @return 转化后的map + */ + public static Map toIdentityMap(Collection collection, Function key, boolean isParallel) { + if (CollUtil.isEmpty(collection)) { + return MapUtil.newHashMap(0); + } + return toMap(collection, (v) -> Opt.ofNullable(v).map(key).get(), Function.identity(), isParallel); + } + + /** + * 将Collection转化为map(value类型与collection的泛型不同)
+ * {@code Collection -----> Map } + * + * @param collection 需要转化的集合 + * @param key E类型转化为K类型的lambda方法 + * @param value E类型转化为V类型的lambda方法 + * @param collection中的泛型 + * @param map中的key类型 + * @param map中的value类型 + * @return 转化后的map + */ + public static Map toMap(Collection collection, Function key, Function value) { + return toMap(collection, key, value, false); + } + + /** + * @param collection 需要转化的集合 + * @param key E类型转化为K类型的lambda方法 + * @param value E类型转化为V类型的lambda方法 + * @param isParallel 是否并行流 + * @param collection中的泛型 + * @param map中的key类型 + * @param map中的value类型 + * @return 转化后的map + */ + public static Map toMap(Collection collection, Function key, Function value, boolean isParallel) { + if (CollUtil.isEmpty(collection)) { + return MapUtil.newHashMap(0); + } + return StreamUtil.of(collection, isParallel) + .collect(HashMap::new, (m, v) -> m.put(key.apply(v), value.apply(v)), HashMap::putAll); + } + + + /** + * 将collection按照规则(比如有相同的班级id)分组成map
+ * {@code Collection -------> Map> } + * + * @param collection 需要分组的集合 + * @param key 分组的规则 + * @param collection中的泛型 + * @param map中的key类型 + * @return 分组后的map + */ + public static Map> groupByKey(Collection collection, Function key) { + return groupByKey(collection, key, false); + } + + /** + * 将collection按照规则(比如有相同的班级id)分组成map
+ * {@code Collection -------> Map> } + * + * @param collection 需要分组的集合 + * @param key 键分组的规则 + * @param isParallel 是否并行流 + * @param collection中的泛型 + * @param map中的key类型 + * @return 分组后的map + */ + public static Map> groupByKey(Collection collection, Function key, boolean isParallel) { + if (CollUtil.isEmpty(collection)) { + return MapUtil.newHashMap(0); + } + return groupBy(collection, key, Collectors.toList(), isParallel); + } + + /** + * 将collection按照两个规则(比如有相同的年级id,班级id)分组成双层map
+ * {@code Collection ---> Map>> } + * + * @param collection 需要分组的集合 + * @param key1 第一个分组的规则 + * @param key2 第二个分组的规则 + * @param 集合元素类型 + * @param 第一个map中的key类型 + * @param 第二个map中的key类型 + * @return 分组后的map + */ + public static Map>> groupBy2Key(Collection collection, Function key1, Function key2) { + return groupBy2Key(collection, key1, key2, false); + } + + + /** + * 将collection按照两个规则(比如有相同的年级id,班级id)分组成双层map
+ * {@code Collection ---> Map>> } + * + * @param collection 需要分组的集合 + * @param key1 第一个分组的规则 + * @param key2 第二个分组的规则 + * @param isParallel 是否并行流 + * @param 集合元素类型 + * @param 第一个map中的key类型 + * @param 第二个map中的key类型 + * @return 分组后的map + */ + public static Map>> groupBy2Key(Collection collection, Function key1, + Function key2, boolean isParallel) { + if (CollUtil.isEmpty(collection)) { + return MapUtil.newHashMap(0); + } + return groupBy(collection, key1, CollectorUtil.groupingBy(key2, Collectors.toList()), isParallel); + } + + /** + * 将collection按照两个规则(比如有相同的年级id,班级id)分组成双层map
+ * {@code Collection ---> Map> } + * + * @param collection 需要分组的集合 + * @param key1 第一个分组的规则 + * @param key2 第二个分组的规则 + * @param 第一个map中的key类型 + * @param 第二个map中的key类型 + * @param collection中的泛型 + * @return 分组后的map + */ + public static Map> group2Map(Collection collection, Function key1, Function key2) { + return group2Map(collection, key1, key2, false); + } + + /** + * 将collection按照两个规则(比如有相同的年级id,班级id)分组成双层map
+ * {@code Collection ---> Map> } + * + * @param collection 需要分组的集合 + * @param key1 第一个分组的规则 + * @param key2 第二个分组的规则 + * @param isParallel 是否并行流 + * @param 第一个map中的key类型 + * @param 第二个map中的key类型 + * @param collection中的泛型 + * @return 分组后的map + */ + public static Map> group2Map(Collection collection, + Function key1, Function key2, boolean isParallel) { + if (CollUtil.isEmpty(collection) || key1 == null || key2 == null) { + return MapUtil.newHashMap(0); + } + return groupBy(collection, key1, CollectorUtil.toMap(key2, Function.identity(), (l, r) -> l), isParallel); + } + + /** + * 将collection按照规则(比如有相同的班级id)分组成map,map中的key为班级id,value为班级名
+ * {@code Collection -------> Map> } + * + * @param collection 需要分组的集合 + * @param key 键分组的规则 + * @param value 值分组的规则 + * @param collection中的泛型 + * @param map中的key类型 + * @param List中的value类型 + * @return 分组后的map + */ + public static Map> groupKeyValue(Collection collection, Function key, + Function value) { + return groupKeyValue(collection, key, value, false); + } + + /** + * 将collection按照规则(比如有相同的班级id)分组成map,map中的key为班级id,value为班级名
+ * {@code Collection -------> Map> } + * + * @param collection 需要分组的集合 + * @param key 键分组的规则 + * @param value 值分组的规则 + * @param isParallel 是否并行流 + * @param collection中的泛型 + * @param map中的key类型 + * @param List中的value类型 + * @return 分组后的map + */ + public static Map> groupKeyValue(Collection collection, Function key, + Function value, boolean isParallel) { + if (CollUtil.isEmpty(collection)) { + return MapUtil.newHashMap(0); + } + return groupBy(collection, key, Collectors.mapping(v -> Opt.ofNullable(v).map(value).orElse(null), Collectors.toList()), isParallel); + } + + /** + * 作为所有groupingBy的公共方法,更接近于原生,灵活性更强 + * + * @param collection 需要分组的集合 + * @param key 第一次分组时需要的key + * @param downstream 分组后需要进行的操作 + * @param collection中的泛型 + * @param map中的key类型 + * @param 后续操作的返回值 + * @return 分组后的map + * @since 5.7.18 + */ + public static Map groupBy(Collection collection, Function key, Collector downstream) { + if (CollUtil.isEmpty(collection)) { + return MapUtil.newHashMap(0); + } + return groupBy(collection, key, downstream, false); + } + + /** + * 作为所有groupingBy的公共方法,更接近于原生,灵活性更强 + * + * @param collection 需要分组的集合 + * @param key 第一次分组时需要的key + * @param downstream 分组后需要进行的操作 + * @param isParallel 是否并行流 + * @param collection中的泛型 + * @param map中的key类型 + * @param 后续操作的返回值 + * @return 分组后的map + * @see Collectors#groupingBy(Function, Collector) + * @since 5.7.18 + */ + public static Map groupBy(Collection collection, Function key, Collector downstream, boolean isParallel) { + if (CollUtil.isEmpty(collection)) { + return MapUtil.newHashMap(0); + } + return StreamUtil.of(collection, isParallel).collect(CollectorUtil.groupingBy(key, downstream)); + } + + /** + * 将collection转化为List集合,但是两者的泛型不同
+ * {@code Collection ------> List } + * + * @param collection 需要转化的集合 + * @param function collection中的泛型转化为list泛型的lambda表达式 + * @param collection中的泛型 + * @param List中的泛型 + * @return 转化后的list + */ + public static List toList(Collection collection, Function function) { + return toList(collection, function, false); + } + + /** + * 将collection转化为List集合,但是两者的泛型不同
+ * {@code Collection ------> List } + * + * @param collection 需要转化的集合 + * @param function collection中的泛型转化为list泛型的lambda表达式 + * @param isParallel 是否并行流 + * @param collection中的泛型 + * @param List中的泛型 + * @return 转化后的list + */ + public static List toList(Collection collection, Function function, boolean isParallel) { + if (CollUtil.isEmpty(collection)) { + return CollUtil.newArrayList(); + } + return StreamUtil.of(collection, isParallel) + .map(function) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + } + + /** + * 将collection转化为Set集合,但是两者的泛型不同
+ * {@code Collection ------> Set } + * + * @param collection 需要转化的集合 + * @param function collection中的泛型转化为set泛型的lambda表达式 + * @param collection中的泛型 + * @param Set中的泛型 + * @return 转化后的Set + */ + public static Set toSet(Collection collection, Function function) { + return toSet(collection, function, false); + } + + /** + * 将collection转化为Set集合,但是两者的泛型不同
+ * {@code Collection ------> Set } + * + * @param collection 需要转化的集合 + * @param function collection中的泛型转化为set泛型的lambda表达式 + * @param isParallel 是否并行流 + * @param collection中的泛型 + * @param Set中的泛型 + * @return 转化后的Set + */ + public static Set toSet(Collection collection, Function function, boolean isParallel) { + if (CollUtil.isEmpty(collection)) { + return CollUtil.newHashSet(); + } + return StreamUtil.of(collection, isParallel) + .map(function) + .filter(Objects::nonNull) + .collect(Collectors.toSet()); + } + + + /** + * 合并两个相同key类型的map + * + * @param map1 第一个需要合并的 map + * @param map2 第二个需要合并的 map + * @param merge 合并的lambda,将key value1 value2合并成最终的类型,注意value可能为空的情况 + * @param map中的key类型 + * @param 第一个 map的value类型 + * @param 第二个 map的value类型 + * @param 最终map的value类型 + * @return 合并后的map + */ + public static Map merge(Map map1, Map map2, BiFunction merge) { + if (MapUtil.isEmpty(map1) && MapUtil.isEmpty(map2)) { + return MapUtil.newHashMap(0); + } else if (MapUtil.isEmpty(map1)) { + map1 = MapUtil.newHashMap(0); + } else if (MapUtil.isEmpty(map2)) { + map2 = MapUtil.newHashMap(0); + } + Set key = new HashSet<>(); + key.addAll(map1.keySet()); + key.addAll(map2.keySet()); + Map map = MapUtil.newHashMap(key.size()); + for (K t : key) { + X x = map1.get(t); + Y y = map2.get(t); + V z = merge.apply(x, y); + if (z != null) { + map.put(t, z); + } + } + return map; + } + +} diff --git a/src/main/java/cn/hutool/core/collection/CollUtil.java b/src/main/java/cn/hutool/core/collection/CollUtil.java new file mode 100644 index 0000000..8635a8c --- /dev/null +++ b/src/main/java/cn/hutool/core/collection/CollUtil.java @@ -0,0 +1,3103 @@ +package cn.hutool.core.collection; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.comparator.CompareUtil; +import cn.hutool.core.comparator.PinyinComparator; +import cn.hutool.core.comparator.PropertyComparator; +import cn.hutool.core.convert.Convert; +import cn.hutool.core.convert.ConverterRegistry; +import cn.hutool.core.exceptions.UtilException; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.lang.Editor; +import cn.hutool.core.lang.Filter; +import cn.hutool.core.lang.Matcher; +import cn.hutool.core.lang.func.Func1; +import cn.hutool.core.lang.hash.Hash32; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.*; + +import java.io.Serializable; +import java.lang.reflect.Type; +import java.util.*; +import java.util.Map.Entry; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.LinkedBlockingDeque; +import java.util.function.BiConsumer; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.function.Supplier; + +/** + * 集合相关工具类 + *

+ * 此工具方法针对{@link Collection}及其实现类封装的工具。 + *

+ * 由于{@link Collection} 实现了{@link Iterable}接口,因此部分工具此类不提供,而是在{@link IterUtil} 中提供 + * + * @author xiaoleilu + * @see IterUtil + * @since 3.1.1 + */ +public class CollUtil { + + /** + * 如果提供的集合为{@code null},返回一个不可变的默认空集合,否则返回原集合
+ * 空集合使用{@link Collections#emptySet()} + * + * @param 集合元素类型 + * @param set 提供的集合,可能为null + * @return 原集合,若为null返回空集合 + * @since 4.6.3 + */ + public static Set emptyIfNull(Set set) { + return (null == set) ? Collections.emptySet() : set; + } + + /** + * 如果提供的集合为{@code null},返回一个不可变的默认空集合,否则返回原集合
+ * 空集合使用{@link Collections#emptyList()} + * + * @param 集合元素类型 + * @param list 提供的集合,可能为null + * @return 原集合,若为null返回空集合 + * @since 4.6.3 + */ + public static List emptyIfNull(List list) { + return (null == list) ? Collections.emptyList() : list; + } + + /** + * 两个集合的并集
+ * 针对一个集合中存在多个相同元素的情况,计算两个集合中此元素的个数,保留最多的个数
+ * 例如:集合1:[a, b, c, c, c],集合2:[a, b, c, c]
+ * 结果:[a, b, c, c, c],此结果中只保留了三个c + * + * @param 集合元素类型 + * @param coll1 集合1 + * @param coll2 集合2 + * @return 并集的集合,返回 {@link ArrayList} + */ + public static Collection union(Collection coll1, Collection coll2) { + if (isEmpty(coll1) && isEmpty(coll2)) { + return new ArrayList<>(); + } + if (isEmpty(coll1)) { + return new ArrayList<>(coll2); + } else if (isEmpty(coll2)) { + return new ArrayList<>(coll1); + } + + final ArrayList list = new ArrayList<>(Math.max(coll1.size(), coll2.size())); + final Map map1 = countMap(coll1); + final Map map2 = countMap(coll2); + final Set elts = newHashSet(coll2); + elts.addAll(coll1); + int m; + for (T t : elts) { + m = Math.max(Convert.toInt(map1.get(t), 0), Convert.toInt(map2.get(t), 0)); + for (int i = 0; i < m; i++) { + list.add(t); + } + } + return list; + } + + /** + * 多个集合的并集
+ * 针对一个集合中存在多个相同元素的情况,计算两个集合中此元素的个数,保留最多的个数
+ * 例如:集合1:[a, b, c, c, c],集合2:[a, b, c, c]
+ * 结果:[a, b, c, c, c],此结果中只保留了三个c + * + * @param 集合元素类型 + * @param coll1 集合1 + * @param coll2 集合2 + * @param otherColls 其它集合 + * @return 并集的集合,返回 {@link ArrayList} + */ + @SafeVarargs + public static Collection union(Collection coll1, Collection coll2, Collection... otherColls) { + Collection union = union(coll1, coll2); + for (Collection coll : otherColls) { + if (isEmpty(coll)) { + continue; + } + union = union(union, coll); + } + return union; + } + + /** + * 多个集合的非重复并集,类似于SQL中的“UNION DISTINCT”
+ * 针对一个集合中存在多个相同元素的情况,只保留一个
+ * 例如:集合1:[a, b, c, c, c],集合2:[a, b, c, c]
+ * 结果:[a, b, c],此结果中只保留了一个c + * + * @param 集合元素类型 + * @param coll1 集合1 + * @param coll2 集合2 + * @param otherColls 其它集合 + * @return 并集的集合,返回 {@link LinkedHashSet} + */ + @SafeVarargs + public static Set unionDistinct(Collection coll1, Collection coll2, Collection... otherColls) { + final Set result; + if (isEmpty(coll1)) { + result = new LinkedHashSet<>(); + } else { + result = new LinkedHashSet<>(coll1); + } + + if (isNotEmpty(coll2)) { + result.addAll(coll2); + } + + if (ArrayUtil.isNotEmpty(otherColls)) { + for (Collection otherColl : otherColls) { + if (isEmpty(otherColl)) { + continue; + } + result.addAll(otherColl); + } + } + + return result; + } + + /** + * 多个集合的完全并集,类似于SQL中的“UNION ALL”
+ * 针对一个集合中存在多个相同元素的情况,保留全部元素
+ * 例如:集合1:[a, b, c, c, c],集合2:[a, b, c, c]
+ * 结果:[a, b, c, c, c, a, b, c, c] + * + * @param 集合元素类型 + * @param coll1 集合1 + * @param coll2 集合2 + * @param otherColls 其它集合 + * @return 并集的集合,返回 {@link ArrayList} + */ + @SafeVarargs + public static List unionAll(Collection coll1, Collection coll2, Collection... otherColls) { + if (CollUtil.isEmpty(coll1) && CollUtil.isEmpty(coll2) && ArrayUtil.isEmpty(otherColls)) { + return new ArrayList<>(0); + } + + // 计算元素总数 + int totalSize = 0; + totalSize += size(coll1); + totalSize += size(coll2); + if (otherColls != null) { + for (final Collection otherColl : otherColls) { + totalSize += size(otherColl); + } + } + + // 根据size创建,防止多次扩容 + final List res = new ArrayList<>(totalSize); + if (coll1 != null) { + res.addAll(coll1); + } + if (coll2 != null) { + res.addAll(coll2); + } + if (otherColls == null) { + return res; + } + + for (final Collection otherColl : otherColls) { + if (otherColl != null) { + res.addAll(otherColl); + } + } + + return res; + } + + /** + * 两个集合的交集
+ * 针对一个集合中存在多个相同元素的情况,计算两个集合中此元素的个数,保留最少的个数
+ * 例如:集合1:[a, b, c, c, c],集合2:[a, b, c, c]
+ * 结果:[a, b, c, c],此结果中只保留了两个c + * + * @param 集合元素类型 + * @param coll1 集合1 + * @param coll2 集合2 + * @return 交集的集合,返回 {@link ArrayList} + */ + public static Collection intersection(Collection coll1, Collection coll2) { + if (isNotEmpty(coll1) && isNotEmpty(coll2)) { + final ArrayList list = new ArrayList<>(Math.min(coll1.size(), coll2.size())); + final Map map1 = countMap(coll1); + final Map map2 = countMap(coll2); + final Set elts = newHashSet(coll2); + int m; + for (T t : elts) { + m = Math.min(Convert.toInt(map1.get(t), 0), Convert.toInt(map2.get(t), 0)); + for (int i = 0; i < m; i++) { + list.add(t); + } + } + return list; + } + + return new ArrayList<>(); + } + + /** + * 多个集合的交集
+ * 针对一个集合中存在多个相同元素的情况,计算两个集合中此元素的个数,保留最少的个数
+ * 例如:集合1:[a, b, c, c, c],集合2:[a, b, c, c]
+ * 结果:[a, b, c, c],此结果中只保留了两个c + * + * @param 集合元素类型 + * @param coll1 集合1 + * @param coll2 集合2 + * @param otherColls 其它集合 + * @return 交集的集合,返回 {@link ArrayList} + */ + @SafeVarargs + public static Collection intersection(Collection coll1, Collection coll2, Collection... otherColls) { + Collection intersection = intersection(coll1, coll2); + if (isEmpty(intersection)) { + return intersection; + } + for (Collection coll : otherColls) { + intersection = intersection(intersection, coll); + if (isEmpty(intersection)) { + return intersection; + } + } + return intersection; + } + + /** + * 多个集合的交集
+ * 针对一个集合中存在多个相同元素的情况,只保留一个
+ * 例如:集合1:[a, b, c, c, c],集合2:[a, b, c, c]
+ * 结果:[a, b, c],此结果中只保留了一个c + * + * @param 集合元素类型 + * @param coll1 集合1 + * @param coll2 集合2 + * @param otherColls 其它集合 + * @return 交集的集合,返回 {@link LinkedHashSet} + * @since 5.3.9 + */ + @SafeVarargs + public static Set intersectionDistinct(Collection coll1, Collection coll2, Collection... otherColls) { + final Set result; + if (isEmpty(coll1) || isEmpty(coll2)) { + // 有一个空集合就直接返回空 + return new LinkedHashSet<>(); + } else { + result = new LinkedHashSet<>(coll1); + } + + if (ArrayUtil.isNotEmpty(otherColls)) { + for (Collection otherColl : otherColls) { + if (isNotEmpty(otherColl)) { + result.retainAll(otherColl); + } else { + // 有一个空集合就直接返回空 + return new LinkedHashSet<>(); + } + } + } + + result.retainAll(coll2); + + return result; + } + + /** + * 两个集合的差集
+ * 针对一个集合中存在多个相同元素的情况,计算两个集合中此元素的个数,保留两个集合中此元素个数差的个数
+ * 例如: + * + *

+	 *     disjunction([a, b, c, c, c], [a, b, c, c]) -》 [c]
+	 *     disjunction([a, b], [])                    -》 [a, b]
+	 *     disjunction([a, b, c], [b, c, d])          -》 [a, d]
+	 * 
+ * 任意一个集合为空,返回另一个集合
+ * 两个集合无差集则返回空集合 + * + * @param 集合元素类型 + * @param coll1 集合1 + * @param coll2 集合2 + * @return 差集的集合,返回 {@link ArrayList} + */ + public static Collection disjunction(Collection coll1, Collection coll2) { + if (isEmpty(coll1)) { + return coll2; + } + if (isEmpty(coll2)) { + return coll1; + } + + final List result = new ArrayList<>(); + final Map map1 = countMap(coll1); + final Map map2 = countMap(coll2); + final Set elts = newHashSet(coll2); + elts.addAll(coll1); + int m; + for (T t : elts) { + m = Math.abs(Convert.toInt(map1.get(t), 0) - Convert.toInt(map2.get(t), 0)); + for (int i = 0; i < m; i++) { + result.add(t); + } + } + return result; + } + + /** + * 计算集合的单差集,即只返回【集合1】中有,但是【集合2】中没有的元素,例如: + * + *
+	 *     subtract([1,2,3,4],[2,3,4,5]) -》 [1]
+	 * 
+ * + * @param coll1 集合1 + * @param coll2 集合2 + * @param 元素类型 + * @return 单差集 + */ + public static Collection subtract(Collection coll1, Collection coll2) { + Collection result = ObjectUtil.clone(coll1); + try { + if (null == result) { + result = CollUtil.create(coll1.getClass()); + result.addAll(coll1); + } + result.removeAll(coll2); + } catch (UnsupportedOperationException e){ + // 针对 coll1 为只读集合的补偿 + result = CollUtil.create(AbstractCollection.class); + result.addAll(coll1); + result.removeAll(coll2); + } + return result; + } + + /** + * 计算集合的单差集,即只返回【集合1】中有,但是【集合2】中没有的元素,例如: + * + *
+	 *     subtractToList([1,2,3,4],[2,3,4,5]) -》 [1]
+	 * 
+ * + * @param coll1 集合1 + * @param coll2 集合2 + * @param 元素类型 + * @return 单差集 + * @since 5.3.5 + */ + public static List subtractToList(Collection coll1, Collection coll2) { + + if (isEmpty(coll1)) { + return ListUtil.empty(); + } + if (isEmpty(coll2)) { + return ListUtil.list(true, coll1); + } + + //将被交数用链表储存,防止因为频繁扩容影响性能 + final List result = new LinkedList<>(); + Set set = new HashSet<>(coll2); + for (T t : coll1) { + if (!set.contains(t)) { + result.add(t); + } + } + return result; + } + + /** + * 判断指定集合是否包含指定值,如果集合为空(null或者空),返回{@code false},否则找到元素返回{@code true} + * + * @param collection 集合 + * @param value 需要查找的值 + * @return 如果集合为空(null或者空),返回{@code false},否则找到元素返回{@code true} + * @throws ClassCastException 如果类型不一致会抛出转换异常 + * @throws NullPointerException 当指定的元素 值为 null ,或集合类不支持null 时抛出该异常 + * @see Collection#contains(Object) + * @since 4.1.10 + */ + public static boolean contains(Collection collection, Object value) { + return isNotEmpty(collection) && collection.contains(value); + } + + /** + * 判断指定集合是否包含指定值,如果集合为空(null或者空),返回{@code false},否则找到元素返回{@code true} + * + * @param collection 集合 + * @param value 需要查找的值 + * @return 果集合为空(null或者空),返回{@code false},否则找到元素返回{@code true} + * @since 5.7.16 + */ + public static boolean safeContains(Collection collection, Object value) { + + try { + return contains(collection, value); + } catch (ClassCastException | NullPointerException e) { + return false; + } + } + + + /** + * 自定义函数判断集合是否包含某类值 + * + * @param collection 集合 + * @param containFunc 自定义判断函数 + * @param 值类型 + * @return 是否包含自定义规则的值 + */ + public static boolean contains(Collection collection, Predicate containFunc) { + if (isEmpty(collection)) { + return false; + } + for (T t : collection) { + if (containFunc.test(t)) { + return true; + } + } + return false; + } + + /** + * 其中一个集合在另一个集合中是否至少包含一个元素,即是两个集合是否至少有一个共同的元素 + * + * @param coll1 集合1 + * @param coll2 集合2 + * @return 其中一个集合在另一个集合中是否至少包含一个元素 + * @see #intersection + * @since 2.1 + */ + public static boolean containsAny(Collection coll1, Collection coll2) { + if (isEmpty(coll1) || isEmpty(coll2)) { + return false; + } + if (coll1.size() < coll2.size()) { + for (Object object : coll1) { + if (coll2.contains(object)) { + return true; + } + } + } else { + for (Object object : coll2) { + if (coll1.contains(object)) { + return true; + } + } + } + return false; + } + + /** + * 集合1中是否包含集合2中所有的元素,即集合2是否为集合1的子集 + * + * @param coll1 集合1 + * @param coll2 集合2 + * @return 集合1中是否包含集合2中所有的元素 + * @since 4.5.12 + */ + public static boolean containsAll(Collection coll1, Collection coll2) { + if (isEmpty(coll1)) { + return isEmpty(coll2); + } + + if (isEmpty(coll2)) { + return true; + } + + if (coll1.size() < coll2.size()) { + return false; + } + + for (Object object : coll2) { + if (!coll1.contains(object)) { + return false; + } + } + return true; + } + + /** + * 根据集合返回一个元素计数的 {@link Map}
+ * 所谓元素计数就是假如这个集合中某个元素出现了n次,那将这个元素做为key,n做为value
+ * 例如:[a,b,c,c,c] 得到:
+ * a: 1
+ * b: 1
+ * c: 3
+ * + * @param 集合元素类型 + * @param collection 集合 + * @return {@link Map} + * @see IterUtil#countMap(Iterator) + */ + public static Map countMap(Iterable collection) { + return IterUtil.countMap(null == collection ? null : collection.iterator()); + } + + /** + * 以 conjunction 为分隔符将集合转换为字符串 + * + * @param 集合元素类型 + * @param iterable {@link Iterable} + * @param conjunction 分隔符 + * @param func 集合元素转换器,将元素转换为字符串 + * @return 连接后的字符串 + * @see IterUtil#join(Iterator, CharSequence, Function) + * @since 5.6.7 + */ + public static String join(Iterable iterable, CharSequence conjunction, Function func) { + if (null == iterable) { + return null; + } + return IterUtil.join(iterable.iterator(), conjunction, func); + } + + /** + * 以 conjunction 为分隔符将集合转换为字符串
+ * 如果集合元素为数组、{@link Iterable}或{@link Iterator},则递归组合其为字符串 + * + * @param 集合元素类型 + * @param iterable {@link Iterable} + * @param conjunction 分隔符 + * @return 连接后的字符串 + * @see IterUtil#join(Iterator, CharSequence) + */ + public static String join(Iterable iterable, CharSequence conjunction) { + if (null == iterable) { + return null; + } + return IterUtil.join(iterable.iterator(), conjunction); + } + + /** + * 以 conjunction 为分隔符将集合转换为字符串 + * + * @param 集合元素类型 + * @param iterable {@link Iterable} + * @param conjunction 分隔符 + * @param prefix 每个元素添加的前缀,null表示不添加 + * @param suffix 每个元素添加的后缀,null表示不添加 + * @return 连接后的字符串 + * @since 5.3.0 + */ + public static String join(Iterable iterable, CharSequence conjunction, String prefix, String suffix) { + if (null == iterable) { + return null; + } + return IterUtil.join(iterable.iterator(), conjunction, prefix, suffix); + } + + /** + * 以 conjunction 为分隔符将集合转换为字符串
+ * 如果集合元素为数组、{@link Iterable}或{@link Iterator},则递归组合其为字符串 + * + * @param 集合元素类型 + * @param iterator 集合 + * @param conjunction 分隔符 + * @return 连接后的字符串 + * @deprecated 请使用IterUtil#join(Iterator, CharSequence) + */ + @Deprecated + public static String join(Iterator iterator, CharSequence conjunction) { + return IterUtil.join(iterator, conjunction); + } + + /** + * 切取部分数据
+ * 切取后的栈将减少这些元素 + * + * @param 集合元素类型 + * @param surplusAlaDatas 原数据 + * @param partSize 每部分数据的长度 + * @return 切取出的数据或null + */ + public static List popPart(Stack surplusAlaDatas, int partSize) { + if (isEmpty(surplusAlaDatas)) { + return ListUtil.empty(); + } + + final List currentAlaDatas = new ArrayList<>(); + int size = surplusAlaDatas.size(); + // 切割 + if (size > partSize) { + for (int i = 0; i < partSize; i++) { + currentAlaDatas.add(surplusAlaDatas.pop()); + } + } else { + for (int i = 0; i < size; i++) { + currentAlaDatas.add(surplusAlaDatas.pop()); + } + } + return currentAlaDatas; + } + + /** + * 切取部分数据
+ * 切取后的栈将减少这些元素 + * + * @param 集合元素类型 + * @param surplusAlaDatas 原数据 + * @param partSize 每部分数据的长度 + * @return 切取出的数据或null + */ + public static List popPart(Deque surplusAlaDatas, int partSize) { + if (isEmpty(surplusAlaDatas)) { + return ListUtil.empty(); + } + + final List currentAlaDatas = new ArrayList<>(); + int size = surplusAlaDatas.size(); + // 切割 + if (size > partSize) { + for (int i = 0; i < partSize; i++) { + currentAlaDatas.add(surplusAlaDatas.pop()); + } + } else { + for (int i = 0; i < size; i++) { + currentAlaDatas.add(surplusAlaDatas.pop()); + } + } + return currentAlaDatas; + } + + // ----------------------------------------------------------------------------------------------- new HashSet + + /** + * 新建一个HashSet + * + * @param 集合元素类型 + * @param ts 元素数组 + * @return HashSet对象 + */ + @SafeVarargs + public static HashSet newHashSet(T... ts) { + return set(false, ts); + } + + /** + * 新建一个LinkedHashSet + * + * @param 集合元素类型 + * @param ts 元素数组 + * @return HashSet对象 + * @since 4.1.10 + */ + @SafeVarargs + public static LinkedHashSet newLinkedHashSet(T... ts) { + return (LinkedHashSet) set(true, ts); + } + + /** + * 新建一个HashSet + * + * @param 集合元素类型 + * @param isSorted 是否有序,有序返回 {@link LinkedHashSet},否则返回 {@link HashSet} + * @param ts 元素数组 + * @return HashSet对象 + */ + @SafeVarargs + public static HashSet set(boolean isSorted, T... ts) { + if (null == ts) { + return isSorted ? new LinkedHashSet<>() : new HashSet<>(); + } + int initialCapacity = Math.max((int) (ts.length / .75f) + 1, 16); + final HashSet set = isSorted ? new LinkedHashSet<>(initialCapacity) : new HashSet<>(initialCapacity); + Collections.addAll(set, ts); + return set; + } + + /** + * 新建一个HashSet + * + * @param 集合元素类型 + * @param collection 集合 + * @return HashSet对象 + */ + public static HashSet newHashSet(Collection collection) { + return newHashSet(false, collection); + } + + /** + * 新建一个HashSet + * + * @param 集合元素类型 + * @param isSorted 是否有序,有序返回 {@link LinkedHashSet},否则返回{@link HashSet} + * @param collection 集合,用于初始化Set + * @return HashSet对象 + */ + public static HashSet newHashSet(boolean isSorted, Collection collection) { + return isSorted ? new LinkedHashSet<>(collection) : new HashSet<>(collection); + } + + /** + * 新建一个HashSet + * + * @param 集合元素类型 + * @param isSorted 是否有序,有序返回 {@link LinkedHashSet},否则返回{@link HashSet} + * @param iter {@link Iterator} + * @return HashSet对象 + * @since 3.0.8 + */ + public static HashSet newHashSet(boolean isSorted, Iterator iter) { + if (null == iter) { + return set(isSorted, (T[]) null); + } + final HashSet set = isSorted ? new LinkedHashSet<>() : new HashSet<>(); + while (iter.hasNext()) { + set.add(iter.next()); + } + return set; + } + + /** + * 新建一个HashSet + * + * @param 集合元素类型 + * @param isSorted 是否有序,有序返回 {@link LinkedHashSet},否则返回{@link HashSet} + * @param enumeration {@link Enumeration} + * @return HashSet对象 + * @since 3.0.8 + */ + public static HashSet newHashSet(boolean isSorted, Enumeration enumeration) { + if (null == enumeration) { + return set(isSorted, (T[]) null); + } + final HashSet set = isSorted ? new LinkedHashSet<>() : new HashSet<>(); + while (enumeration.hasMoreElements()) { + set.add(enumeration.nextElement()); + } + return set; + } + + // ----------------------------------------------------------------------------------------------- List + + /** + * 新建一个空List + * + * @param 集合元素类型 + * @param isLinked 是否新建LinkedList + * @return List对象 + * @since 4.1.2 + */ + public static List list(boolean isLinked) { + return ListUtil.list(isLinked); + } + + /** + * 新建一个List + * + * @param 集合元素类型 + * @param isLinked 是否新建LinkedList + * @param values 数组 + * @return List对象 + * @since 4.1.2 + */ + @SafeVarargs + public static List list(boolean isLinked, T... values) { + return ListUtil.list(isLinked, values); + } + + /** + * 新建一个List + * + * @param 集合元素类型 + * @param isLinked 是否新建LinkedList + * @param collection 集合 + * @return List对象 + * @since 4.1.2 + */ + public static List list(boolean isLinked, Collection collection) { + return ListUtil.list(isLinked, collection); + } + + /** + * 新建一个List
+ * 提供的参数为null时返回空{@link ArrayList} + * + * @param 集合元素类型 + * @param isLinked 是否新建LinkedList + * @param iterable {@link Iterable} + * @return List对象 + * @since 4.1.2 + */ + public static List list(boolean isLinked, Iterable iterable) { + return ListUtil.list(isLinked, iterable); + } + + /** + * 新建一个ArrayList
+ * 提供的参数为null时返回空{@link ArrayList} + * + * @param 集合元素类型 + * @param isLinked 是否新建LinkedList + * @param iter {@link Iterator} + * @return ArrayList对象 + * @since 4.1.2 + */ + public static List list(boolean isLinked, Iterator iter) { + return ListUtil.list(isLinked, iter); + } + + /** + * 新建一个List
+ * 提供的参数为null时返回空{@link ArrayList} + * + * @param 集合元素类型 + * @param isLinked 是否新建LinkedList + * @param enumeration {@link Enumeration} + * @return ArrayList对象 + * @since 3.0.8 + */ + public static List list(boolean isLinked, Enumeration enumeration) { + return ListUtil.list(isLinked, enumeration); + } + + /** + * 新建一个ArrayList + * + * @param 集合元素类型 + * @param values 数组 + * @return ArrayList对象 + * @see #toList(Object[]) + */ + @SafeVarargs + public static ArrayList newArrayList(T... values) { + return ListUtil.toList(values); + } + + /** + * 数组转为ArrayList + * + * @param 集合元素类型 + * @param values 数组 + * @return ArrayList对象 + * @since 4.0.11 + */ + @SafeVarargs + public static ArrayList toList(T... values) { + return ListUtil.toList(values); + } + + /** + * 新建一个ArrayList + * + * @param 集合元素类型 + * @param collection 集合 + * @return ArrayList对象 + */ + public static ArrayList newArrayList(Collection collection) { + return ListUtil.toList(collection); + } + + /** + * 新建一个ArrayList
+ * 提供的参数为null时返回空{@link ArrayList} + * + * @param 集合元素类型 + * @param iterable {@link Iterable} + * @return ArrayList对象 + * @since 3.1.0 + */ + public static ArrayList newArrayList(Iterable iterable) { + return ListUtil.toList(iterable); + } + + /** + * 新建一个ArrayList
+ * 提供的参数为null时返回空{@link ArrayList} + * + * @param 集合元素类型 + * @param iterator {@link Iterator} + * @return ArrayList对象 + * @since 3.0.8 + */ + public static ArrayList newArrayList(Iterator iterator) { + return ListUtil.toList(iterator); + } + + /** + * 新建一个ArrayList
+ * 提供的参数为null时返回空{@link ArrayList} + * + * @param 集合元素类型 + * @param enumeration {@link Enumeration} + * @return ArrayList对象 + * @since 3.0.8 + */ + public static ArrayList newArrayList(Enumeration enumeration) { + return ListUtil.toList(enumeration); + } + + // ----------------------------------------------------------------------new LinkedList + + /** + * 新建LinkedList + * + * @param values 数组 + * @param 类型 + * @return LinkedList + * @since 4.1.2 + */ + @SafeVarargs + public static LinkedList newLinkedList(T... values) { + return ListUtil.toLinkedList(values); + } + + /** + * 新建一个CopyOnWriteArrayList + * + * @param 集合元素类型 + * @param collection 集合 + * @return {@link CopyOnWriteArrayList} + */ + public static CopyOnWriteArrayList newCopyOnWriteArrayList(Collection collection) { + return ListUtil.toCopyOnWriteArrayList(collection); + } + + /** + * 新建{@link BlockingQueue}
+ * 在队列为空时,获取元素的线程会等待队列变为非空。当队列满时,存储元素的线程会等待队列可用。 + * + * @param 集合类型 + * @param capacity 容量 + * @param isLinked 是否为链表形式 + * @return {@link BlockingQueue} + * @since 3.3.0 + */ + public static BlockingQueue newBlockingQueue(int capacity, boolean isLinked) { + final BlockingQueue queue; + if (isLinked) { + queue = new LinkedBlockingDeque<>(capacity); + } else { + queue = new ArrayBlockingQueue<>(capacity); + } + return queue; + } + + /** + * 创建新的集合对象 + * + * @param 集合类型 + * @param collectionType 集合类型 + * @return 集合类型对应的实例 + * @since 3.0.8 + */ + public static Collection create(Class collectionType) { + return create(collectionType, null); + } + + /** + * 创建新的集合对象,返回具体的泛型集合 + * + * @param 集合元素类型 + * @param collectionType 集合类型,rawtype 如 ArrayList.class, EnumSet.class ... + * @param elementType 集合元素类型 + * @return 集合类型对应的实例 + * @since v5 + */ + @SuppressWarnings({"unchecked", "rawtypes"}) + public static Collection create(Class collectionType, Class elementType) { + final Collection list; + if (collectionType.isAssignableFrom(AbstractCollection.class)) { + // 抽象集合默认使用ArrayList + list = new ArrayList<>(); + } + + // Set + else if (collectionType.isAssignableFrom(HashSet.class)) { + list = new HashSet<>(); + } else if (collectionType.isAssignableFrom(LinkedHashSet.class)) { + list = new LinkedHashSet<>(); + } else if (collectionType.isAssignableFrom(TreeSet.class)) { + list = new TreeSet<>((o1, o2) -> { + // 优先按照对象本身比较,如果没有实现比较接口,默认按照toString内容比较 + if (o1 instanceof Comparable) { + return ((Comparable) o1).compareTo(o2); + } + return CompareUtil.compare(o1.toString(), o2.toString()); + }); + } else if (collectionType.isAssignableFrom(EnumSet.class)) { + list = (Collection) EnumSet.noneOf(Assert.notNull((Class) elementType)); + } + + // List + else if (collectionType.isAssignableFrom(ArrayList.class)) { + list = new ArrayList<>(); + } else if (collectionType.isAssignableFrom(LinkedList.class)) { + list = new LinkedList<>(); + } + + // Others,直接实例化 + else { + try { + list = (Collection) ReflectUtil.newInstance(collectionType); + } catch (final Exception e) { + // 无法创建当前类型的对象,尝试创建父类型对象 + final Class superclass = collectionType.getSuperclass(); + if (null != superclass && collectionType != superclass) { + return create(superclass); + } + throw new UtilException(e); + } + } + return list; + } + + /** + * 去重集合 + * + * @param 集合元素类型 + * @param collection 集合 + * @return {@link ArrayList} + */ + public static ArrayList distinct(Collection collection) { + if (isEmpty(collection)) { + return new ArrayList<>(); + } else if (collection instanceof Set) { + return new ArrayList<>(collection); + } else { + return new ArrayList<>(new LinkedHashSet<>(collection)); + } + } + + /** + * 根据函数生成的KEY去重集合,如根据Bean的某个或者某些字段完成去重。
+ * 去重可选是保留最先加入的值还是后加入的值 + * + * @param 集合元素类型 + * @param 唯一键类型 + * @param collection 集合 + * @param uniqueGenerator 唯一键生成器 + * @param override 是否覆盖模式,如果为{@code true},加入的新值会覆盖相同key的旧值,否则会忽略新加值 + * @return {@link ArrayList} + * @since 5.8.0 + */ + public static List distinct(Collection collection, Function uniqueGenerator, boolean override) { + if (isEmpty(collection)) { + return new ArrayList<>(); + } + + final UniqueKeySet set = new UniqueKeySet<>(true, uniqueGenerator); + if (override) { + set.addAll(collection); + } else { + set.addAllIfAbsent(collection); + } + return new ArrayList<>(set); + } + + /** + * 截取列表的部分 + * + * @param 集合元素类型 + * @param list 被截取的数组 + * @param start 开始位置(包含) + * @param end 结束位置(不包含) + * @return 截取后的数组,当开始位置超过最大时,返回空的List + * @see ListUtil#sub(List, int, int) + */ + public static List sub(List list, int start, int end) { + return ListUtil.sub(list, start, end); + } + + /** + * 截取列表的部分 + * + * @param 集合元素类型 + * @param list 被截取的数组 + * @param start 开始位置(包含) + * @param end 结束位置(不包含) + * @param step 步进 + * @return 截取后的数组,当开始位置超过最大时,返回空的List + * @see ListUtil#sub(List, int, int, int) + * @since 4.0.6 + */ + public static List sub(List list, int start, int end, int step) { + return ListUtil.sub(list, start, end, step); + } + + /** + * 截取集合的部分 + * + * @param 集合元素类型 + * @param collection 被截取的数组 + * @param start 开始位置(包含) + * @param end 结束位置(不包含) + * @return 截取后的数组,当开始位置超过最大时,返回null + */ + public static List sub(Collection collection, int start, int end) { + return sub(collection, start, end, 1); + } + + /** + * 截取集合的部分 + * + * @param 集合元素类型 + * @param collection 被截取的数组 + * @param start 开始位置(包含) + * @param end 结束位置(不包含) + * @param step 步进 + * @return 截取后的数组,当开始位置超过最大时,返回空集合 + * @since 4.0.6 + */ + public static List sub(Collection collection, int start, int end, int step) { + if (isEmpty(collection)) { + return ListUtil.empty(); + } + + final List list = collection instanceof List ? (List) collection : ListUtil.toList(collection); + return sub(list, start, end, step); + } + + /** + * 对集合按照指定长度分段,每一个段为单独的集合,返回这个集合的列表 + *

+ * 需要特别注意的是,此方法调用{@link List#subList(int, int)}切分List, + * 此方法返回的是原List的视图,也就是说原List有变更,切分后的结果也会变更。 + *

+ * + * @param 集合元素类型 + * @param list 列表 + * @param size 每个段的长度 + * @return 分段列表 + * @since 5.4.5 + * @deprecated 请使用 {@link ListUtil#partition(List, int)} + */ + @Deprecated + public static List> splitList(List list, int size) { + return ListUtil.partition(list, size); + } + + /** + * 对集合按照指定长度分段,每一个段为单独的集合,返回这个集合的列表 + * + * @param 集合元素类型 + * @param collection 集合 + * @param size 每个段的长度 + * @return 分段列表 + */ + public static List> split(Collection collection, int size) { + final List> result = new ArrayList<>(); + if (CollUtil.isEmpty(collection)) { + return result; + } + + ArrayList subList = new ArrayList<>(size); + for (T t : collection) { + if (subList.size() >= size) { + result.add(subList); + subList = new ArrayList<>(size); + } + subList.add(t); + } + result.add(subList); + return result; + } + + /** + * 编辑,此方法产生一个新集合
+ * 编辑过程通过传入的Editor实现来返回需要的元素内容,这个Editor实现可以实现以下功能: + * + *
+	 * 1、过滤出需要的对象,如果返回null表示这个元素对象抛弃
+	 * 2、修改元素对象,返回集合中为修改后的对象
+	 * 
+ * + * @param 集合元素类型 + * @param collection 集合 + * @param editor 编辑器接口,{@code null}返回原集合 + * @return 过滤后的集合 + */ + public static Collection edit(Collection collection, Editor editor) { + if (null == collection || null == editor) { + return collection; + } + + final Collection collection2 = create(collection.getClass()); + if (isEmpty(collection)) { + return collection2; + } + + T modified; + for (T t : collection) { + modified = editor.edit(t); + if (null != modified) { + collection2.add(modified); + } + } + return collection2; + } + + /** + * 过滤
+ * 过滤过程通过传入的Filter实现来过滤返回需要的元素内容,这个Filter实现可以实现以下功能: + * + *
+	 * 1、过滤出需要的对象,{@link Filter#accept(Object)}方法返回true的对象将被加入结果集合中
+	 * 
+ * + * @param 集合元素类型 + * @param collection 集合 + * @param filter 过滤器,{@code null}返回原集合 + * @return 过滤后的数组 + * @since 3.1.0 + */ + public static Collection filterNew(Collection collection, Filter filter) { + if (null == collection || null == filter) { + return collection; + } + return edit(collection, t -> filter.accept(t) ? t : null); + } + + /** + * 去掉集合中的多个元素,此方法直接修改原集合 + * + * @param 集合类型 + * @param 集合元素类型 + * @param collection 集合 + * @param elesRemoved 被去掉的元素数组 + * @return 原集合 + * @since 4.1.0 + */ + @SuppressWarnings("unchecked") + public static , E> T removeAny(T collection, E... elesRemoved) { + collection.removeAll(newHashSet(elesRemoved)); + return collection; + } + + /** + * 去除指定元素,此方法直接修改原集合 + * + * @param 集合类型 + * @param 集合元素类型 + * @param collection 集合 + * @param filter 过滤器 + * @return 处理后的集合 + * @since 4.6.5 + */ + public static , E> T filter(T collection, final Filter filter) { + return IterUtil.filter(collection, filter); + } + + /** + * 去除{@code null} 元素,此方法直接修改原集合 + * + * @param 集合类型 + * @param 集合元素类型 + * @param collection 集合 + * @return 处理后的集合 + * @since 3.2.2 + */ + public static , E> T removeNull(T collection) { + return filter(collection, Objects::nonNull); + } + + /** + * 去除{@code null}或者"" 元素,此方法直接修改原集合 + * + * @param 集合类型 + * @param 集合元素类型 + * @param collection 集合 + * @return 处理后的集合 + * @since 3.2.2 + */ + public static , E extends CharSequence> T removeEmpty(T collection) { + return filter(collection, StrUtil::isNotEmpty); + } + + /** + * 去除{@code null}或者""或者空白字符串 元素,此方法直接修改原集合 + * + * @param 集合类型 + * @param 集合元素类型 + * @param collection 集合 + * @return 处理后的集合 + * @since 3.2.2 + */ + public static , E extends CharSequence> T removeBlank(T collection) { + return filter(collection, StrUtil::isNotBlank); + } + + /** + * 移除集合中的多个元素,并将结果存放到指定的集合 + * 此方法直接修改原集合 + * + * @param 集合类型 + * @param 集合元素类型 + * @param resultCollection 存放移除结果的集合 + * @param targetCollection 被操作移除元素的集合 + * @param predicate 用于是否移除判断的过滤器 + * @return 移除结果的集合 + * @since 5.7.17 + */ + public static , E> T removeWithAddIf(T targetCollection, T resultCollection, Predicate predicate) { + Objects.requireNonNull(predicate); + final Iterator each = targetCollection.iterator(); + while (each.hasNext()) { + E next = each.next(); + if (predicate.test(next)) { + resultCollection.add(next); + each.remove(); + } + } + return resultCollection; + } + + /** + * 移除集合中的多个元素,并将结果存放到生成的新集合中后返回
+ * 此方法直接修改原集合 + * + * @param 集合类型 + * @param 集合元素类型 + * @param targetCollection 被操作移除元素的集合 + * @param predicate 用于是否移除判断的过滤器 + * @return 移除结果的集合 + * @since 5.7.17 + */ + public static , E> List removeWithAddIf(T targetCollection, Predicate predicate) { + final List removed = new ArrayList<>(); + removeWithAddIf(targetCollection, removed, predicate); + return removed; + } + + /** + * 通过Editor抽取集合元素中的某些值返回为新列表
+ * 例如提供的是一个Bean列表,通过Editor接口实现获取某个字段值,返回这个字段值组成的新列表 + * + * @param collection 原集合 + * @param editor 编辑器 + * @return 抽取后的新列表 + */ + public static List extract(Iterable collection, Editor editor) { + return extract(collection, editor, false); + } + + /** + * 通过Editor抽取集合元素中的某些值返回为新列表
+ * 例如提供的是一个Bean列表,通过Editor接口实现获取某个字段值,返回这个字段值组成的新列表 + * + * @param collection 原集合 + * @param editor 编辑器 + * @param ignoreNull 是否忽略空值 + * @return 抽取后的新列表 + * @see #map(Iterable, Function, boolean) + * @since 4.5.7 + */ + public static List extract(Iterable collection, Editor editor, boolean ignoreNull) { + return map(collection, editor::edit, ignoreNull); + } + + /** + * 通过func自定义一个规则,此规则将原集合中的元素转换成新的元素,生成新的列表返回
+ * 例如提供的是一个Bean列表,通过Function接口实现获取某个字段值,返回这个字段值组成的新列表 + * + * @param 集合元素类型 + * @param 返回集合元素类型 + * @param collection 原集合 + * @param func 编辑函数 + * @param ignoreNull 是否忽略空值,这里的空值包括函数处理前和处理后的null值 + * @return 抽取后的新列表 + * @since 5.3.5 + */ + public static List map(Iterable collection, Function func, boolean ignoreNull) { + final List fieldValueList = new ArrayList<>(); + if (null == collection) { + return fieldValueList; + } + + R value; + for (T t : collection) { + if (null == t && ignoreNull) { + continue; + } + value = func.apply(t); + if (null == value && ignoreNull) { + continue; + } + fieldValueList.add(value); + } + return fieldValueList; + } + + /** + * 获取给定Bean列表中指定字段名对应字段值的列表
+ * 列表元素支持Bean与Map + * + * @param collection Bean集合或Map集合 + * @param fieldName 字段名或map的键 + * @return 字段值列表 + * @since 3.1.0 + */ + public static List getFieldValues(Iterable collection, final String fieldName) { + return getFieldValues(collection, fieldName, false); + } + + /** + * 获取给定Bean列表中指定字段名对应字段值的列表
+ * 列表元素支持Bean与Map + * + * @param collection Bean集合或Map集合 + * @param fieldName 字段名或map的键 + * @param ignoreNull 是否忽略值为{@code null}的字段 + * @return 字段值列表 + * @since 4.5.7 + */ + public static List getFieldValues(Iterable collection, final String fieldName, boolean ignoreNull) { + return map(collection, bean -> { + if (bean instanceof Map) { + return ((Map) bean).get(fieldName); + } else { + return ReflectUtil.getFieldValue(bean, fieldName); + } + }, ignoreNull); + } + + /** + * 获取给定Bean列表中指定字段名对应字段值的列表
+ * 列表元素支持Bean与Map + * + * @param 元素类型 + * @param collection Bean集合或Map集合 + * @param fieldName 字段名或map的键 + * @param elementType 元素类型类 + * @return 字段值列表 + * @since 4.5.6 + */ + public static List getFieldValues(Iterable collection, final String fieldName, final Class elementType) { + List fieldValues = getFieldValues(collection, fieldName); + return Convert.toList(elementType, fieldValues); + } + + /** + * 字段值与列表值对应的Map,常用于元素对象中有唯一ID时需要按照这个ID查找对象的情况
+ * 例如:车牌号 =》车 + * + * @param 字段名对应值得类型,不确定请使用Object + * @param 对象类型 + * @param iterable 对象列表 + * @param fieldName 字段名(会通过反射获取其值) + * @return 某个字段值与对象对应Map + * @since 5.0.6 + */ + public static Map fieldValueMap(Iterable iterable, String fieldName) { + return IterUtil.fieldValueMap(IterUtil.getIter(iterable), fieldName); + } + + /** + * 两个字段值组成新的Map + * + * @param 字段名对应值得类型,不确定请使用Object + * @param 值类型,不确定使用Object + * @param iterable 对象列表 + * @param fieldNameForKey 做为键的字段名(会通过反射获取其值) + * @param fieldNameForValue 做为值的字段名(会通过反射获取其值) + * @return 某个字段值与对象对应Map + * @since 5.0.6 + */ + public static Map fieldValueAsMap(Iterable iterable, String fieldNameForKey, String fieldNameForValue) { + return IterUtil.fieldValueAsMap(IterUtil.getIter(iterable), fieldNameForKey, fieldNameForValue); + } + + /** + * 查找第一个匹配元素对象 + * + * @param 集合元素类型 + * @param collection 集合 + * @param filter 过滤器,满足过滤条件的第一个元素将被返回 + * @return 满足过滤条件的第一个元素 + * @since 3.1.0 + */ + public static T findOne(Iterable collection, Filter filter) { + if (null != collection) { + for (T t : collection) { + if (filter.accept(t)) { + return t; + } + } + } + return null; + } + + /** + * 查找第一个匹配元素对象
+ * 如果集合元素是Map,则比对键和值是否相同,相同则返回
+ * 如果为普通Bean,则通过反射比对元素字段名对应的字段值是否相同,相同则返回
+ * 如果给定字段值参数是{@code null} 且元素对象中的字段值也为{@code null}则认为相同 + * + * @param 集合元素类型 + * @param collection 集合,集合元素可以是Bean或者Map + * @param fieldName 集合元素对象的字段名或map的键 + * @param fieldValue 集合元素对象的字段值或map的值 + * @return 满足条件的第一个元素 + * @since 3.1.0 + */ + public static T findOneByField(Iterable collection, final String fieldName, final Object fieldValue) { + return findOne(collection, t -> { + if (t instanceof Map) { + final Map map = (Map) t; + final Object value = map.get(fieldName); + return ObjectUtil.equal(value, fieldValue); + } + + // 普通Bean + final Object value = ReflectUtil.getFieldValue(t, fieldName); + return ObjectUtil.equal(value, fieldValue); + }); + } + + /** + * 集合中匹配规则的数量 + * + * @param 集合元素类型 + * @param iterable {@link Iterable} + * @param matcher 匹配器,为空则全部匹配 + * @return 匹配数量 + */ + public static int count(Iterable iterable, Matcher matcher) { + int count = 0; + if (null != iterable) { + for (T t : iterable) { + if (null == matcher || matcher.match(t)) { + count++; + } + } + } + return count; + } + + /** + * 获取匹配规则定义中匹配到元素的第一个位置
+ * 此方法对于某些无序集合的位置信息,以转换为数组后的位置为准。 + * + * @param 元素类型 + * @param collection 集合 + * @param matcher 匹配器,为空则全部匹配 + * @return 第一个位置 + * @since 5.6.6 + */ + public static int indexOf(Collection collection, Matcher matcher) { + if (isNotEmpty(collection)) { + int index = 0; + for (T t : collection) { + if (null == matcher || matcher.match(t)) { + return index; + } + index++; + } + } + return -1; + } + + /** + * 获取匹配规则定义中匹配到元素的最后位置
+ * 此方法对于某些无序集合的位置信息,以转换为数组后的位置为准。 + * + * @param 元素类型 + * @param collection 集合 + * @param matcher 匹配器,为空则全部匹配 + * @return 最后一个位置 + * @since 5.6.6 + */ + public static int lastIndexOf(Collection collection, Matcher matcher) { + if (collection instanceof List) { + // List的查找最后一个有优化算法 + return ListUtil.lastIndexOf((List) collection, matcher); + } + int matchIndex = -1; + if (isNotEmpty(collection)) { + int index = 0; + for (T t : collection) { + if (null == matcher || matcher.match(t)) { + matchIndex = index; + } + index++; + } + } + return matchIndex; + } + + /** + * 获取匹配规则定义中匹配到元素的所有位置
+ * 此方法对于某些无序集合的位置信息,以转换为数组后的位置为准。 + * + * @param 元素类型 + * @param collection 集合 + * @param matcher 匹配器,为空则全部匹配 + * @return 位置数组 + * @since 5.2.5 + */ + public static int[] indexOfAll(Collection collection, Matcher matcher) { + final List indexList = new ArrayList<>(); + if (null != collection) { + int index = 0; + for (T t : collection) { + if (null == matcher || matcher.match(t)) { + indexList.add(index); + } + index++; + } + } + return Convert.convert(int[].class, indexList); + } + + // ---------------------------------------------------------------------- isEmpty + + /** + * 集合是否为空 + * + * @param collection 集合 + * @return 是否为空 + */ + public static boolean isEmpty(Collection collection) { + return collection == null || collection.isEmpty(); + } + + /** + * 如果给定集合为空,返回默认集合 + * + * @param 集合类型 + * @param 集合元素类型 + * @param collection 集合 + * @param defaultCollection 默认数组 + * @return 非空(empty)的原集合或默认集合 + * @since 4.6.9 + */ + public static , E> T defaultIfEmpty(T collection, T defaultCollection) { + return isEmpty(collection) ? defaultCollection : collection; + } + + /** + * 如果给定集合为空,返回默认集合 + * + * @param 集合类型 + * @param 集合元素类型 + * @param collection 集合 + * @param supplier 默认值懒加载函数 + * @return 非空(empty)的原集合或默认集合 + * @since 5.7.15 + */ + public static , E> T defaultIfEmpty(T collection, Supplier supplier) { + return isEmpty(collection) ? supplier.get() : collection; + } + + /** + * Iterable是否为空 + * + * @param iterable Iterable对象 + * @return 是否为空 + * @see IterUtil#isEmpty(Iterable) + */ + public static boolean isEmpty(Iterable iterable) { + return IterUtil.isEmpty(iterable); + } + + /** + * Iterator是否为空 + * + * @param Iterator Iterator对象 + * @return 是否为空 + * @see IterUtil#isEmpty(Iterator) + */ + public static boolean isEmpty(Iterator Iterator) { + return IterUtil.isEmpty(Iterator); + } + + /** + * Enumeration是否为空 + * + * @param enumeration {@link Enumeration} + * @return 是否为空 + */ + public static boolean isEmpty(Enumeration enumeration) { + return null == enumeration || !enumeration.hasMoreElements(); + } + + /** + * Map是否为空 + * + * @param map 集合 + * @return 是否为空 + * @see MapUtil#isEmpty(Map) + * @since 5.7.4 + */ + public static boolean isEmpty(Map map) { + return MapUtil.isEmpty(map); + } + + // ---------------------------------------------------------------------- isNotEmpty + + /** + * 集合是否为非空 + * + * @param collection 集合 + * @return 是否为非空 + */ + public static boolean isNotEmpty(Collection collection) { + return !isEmpty(collection); + } + + /** + * Iterable是否为空 + * + * @param iterable Iterable对象 + * @return 是否为空 + * @see IterUtil#isNotEmpty(Iterable) + */ + public static boolean isNotEmpty(Iterable iterable) { + return IterUtil.isNotEmpty(iterable); + } + + /** + * Iterator是否为空 + * + * @param Iterator Iterator对象 + * @return 是否为空 + * @see IterUtil#isNotEmpty(Iterator) + */ + public static boolean isNotEmpty(Iterator Iterator) { + return IterUtil.isNotEmpty(Iterator); + } + + /** + * Enumeration是否为空 + * + * @param enumeration {@link Enumeration} + * @return 是否为空 + */ + public static boolean isNotEmpty(Enumeration enumeration) { + return null != enumeration && enumeration.hasMoreElements(); + } + + /** + * 是否包含{@code null}元素 + * + * @param iterable 被检查的Iterable对象,如果为{@code null} 返回true + * @return 是否包含{@code null}元素 + * @see IterUtil#hasNull(Iterable) + * @since 3.0.7 + */ + public static boolean hasNull(Iterable iterable) { + return IterUtil.hasNull(iterable); + } + + /** + * Map是否为非空 + * + * @param map 集合 + * @return 是否为非空 + * @see MapUtil#isNotEmpty(Map) + * @since 5.7.4 + */ + public static boolean isNotEmpty(Map map) { + return MapUtil.isNotEmpty(map); + } + + // ---------------------------------------------------------------------- zip + + /** + * 映射键值(参考Python的zip()函数)
+ * 例如:
+ * keys = a,b,c,d
+ * values = 1,2,3,4
+ * delimiter = , 则得到的Map是 {a=1, b=2, c=3, d=4}
+ * 如果两个数组长度不同,则只对应最短部分 + * + * @param keys 键列表 + * @param values 值列表 + * @param delimiter 分隔符 + * @param isOrder 是否有序 + * @return Map + * @since 3.0.4 + */ + public static Map zip(String keys, String values, String delimiter, boolean isOrder) { + return ArrayUtil.zip(StrUtil.splitToArray(keys, delimiter), StrUtil.splitToArray(values, delimiter), isOrder); + } + + /** + * 映射键值(参考Python的zip()函数),返回Map无序
+ * 例如:
+ * keys = a,b,c,d
+ * values = 1,2,3,4
+ * delimiter = , 则得到的Map是 {a=1, b=2, c=3, d=4}
+ * 如果两个数组长度不同,则只对应最短部分 + * + * @param keys 键列表 + * @param values 值列表 + * @param delimiter 分隔符 + * @return Map + */ + public static Map zip(String keys, String values, String delimiter) { + return zip(keys, values, delimiter, false); + } + + /** + * 映射键值(参考Python的zip()函数)
+ * 例如:
+ * keys = [a,b,c,d]
+ * values = [1,2,3,4]
+ * 则得到的Map是 {a=1, b=2, c=3, d=4}
+ * 如果两个数组长度不同,则只对应最短部分 + * + * @param 键类型 + * @param 值类型 + * @param keys 键列表 + * @param values 值列表 + * @return Map + */ + public static Map zip(Collection keys, Collection values) { + if (isEmpty(keys) || isEmpty(values)) { + return MapUtil.empty(); + } + + int entryCount = Math.min(keys.size(), values.size()); + final Map map = MapUtil.newHashMap(entryCount); + + final Iterator keyIterator = keys.iterator(); + final Iterator valueIterator = values.iterator(); + while (entryCount > 0) { + map.put(keyIterator.next(), valueIterator.next()); + entryCount--; + } + + return map; + } + + /** + * 将Entry集合转换为HashMap + * + * @param 键类型 + * @param 值类型 + * @param entryIter entry集合 + * @return Map + * @see IterUtil#toMap(Iterable) + */ + public static HashMap toMap(Iterable> entryIter) { + return IterUtil.toMap(entryIter); + } + + /** + * 将数组转换为Map(HashMap),支持数组元素类型为: + * + *
+	 * Map.Entry
+	 * 长度大于1的数组(取前两个值),如果不满足跳过此元素
+	 * Iterable 长度也必须大于1(取前两个值),如果不满足跳过此元素
+	 * Iterator 长度也必须大于1(取前两个值),如果不满足跳过此元素
+	 * 
+ * + *
+	 * Map<Object, Object> colorMap = CollectionUtil.toMap(new String[][] {{
+	 *     {"RED", "#FF0000"},
+	 *     {"GREEN", "#00FF00"},
+	 *     {"BLUE", "#0000FF"}});
+	 * 
+ *

+ * 参考:commons-lang + * + * @param array 数组。元素类型为Map.Entry、数组、Iterable、Iterator + * @return {@link HashMap} + * @see MapUtil#of(Object[]) + * @since 3.0.8 + */ + public static HashMap toMap(Object[] array) { + return MapUtil.of(array); + } + + /** + * 将集合转换为排序后的TreeSet + * + * @param 集合元素类型 + * @param collection 集合 + * @param comparator 比较器 + * @return treeSet + */ + public static TreeSet toTreeSet(Collection collection, Comparator comparator) { + final TreeSet treeSet = new TreeSet<>(comparator); + treeSet.addAll(collection); + return treeSet; + } + + /** + * Iterator转换为Enumeration + *

+ * Adapt the specified {@link Iterator} to the {@link Enumeration} interface. + * + * @param 集合元素类型 + * @param iter {@link Iterator} + * @return {@link Enumeration} + */ + public static Enumeration asEnumeration(Iterator iter) { + return new IteratorEnumeration<>(iter); + } + + /** + * Enumeration转换为Iterator + *

+ * Adapt the specified {@code Enumeration} to the {@code Iterator} interface + * + * @param 集合元素类型 + * @param e {@link Enumeration} + * @return {@link Iterator} + * @see IterUtil#asIterator(Enumeration) + */ + public static Iterator asIterator(Enumeration e) { + return IterUtil.asIterator(e); + } + + /** + * {@link Iterator} 转为 {@link Iterable} + * + * @param 元素类型 + * @param iter {@link Iterator} + * @return {@link Iterable} + * @see IterUtil#asIterable(Iterator) + */ + public static Iterable asIterable(final Iterator iter) { + return IterUtil.asIterable(iter); + } + + /** + * {@link Iterable}转为{@link Collection}
+ * 首先尝试强转,强转失败则构建一个新的{@link ArrayList} + * + * @param 集合元素类型 + * @param iterable {@link Iterable} + * @return {@link Collection} 或者 {@link ArrayList} + * @since 3.0.9 + */ + public static Collection toCollection(Iterable iterable) { + return (iterable instanceof Collection) ? (Collection) iterable : newArrayList(iterable.iterator()); + } + + /** + * 行转列,合并相同的键,值合并为列表
+ * 将Map列表中相同key的值组成列表做为Map的value
+ * 是{@link #toMapList(Map)}的逆方法
+ * 比如传入数据: + * + *

+	 * [
+	 *  {a: 1, b: 1, c: 1}
+	 *  {a: 2, b: 2}
+	 *  {a: 3, b: 3}
+	 *  {a: 4}
+	 * ]
+	 * 
+ *

+ * 结果是: + * + *

+	 * {
+	 *   a: [1,2,3,4]
+	 *   b: [1,2,3,]
+	 *   c: [1]
+	 * }
+	 * 
+ * + * @param 键类型 + * @param 值类型 + * @param mapList Map列表 + * @return Map + * @see MapUtil#toListMap(Iterable) + */ + public static Map> toListMap(Iterable> mapList) { + return MapUtil.toListMap(mapList); + } + + /** + * 列转行。将Map中值列表分别按照其位置与key组成新的map。
+ * 是{@link #toListMap(Iterable)}的逆方法
+ * 比如传入数据: + * + *
+	 * {
+	 *   a: [1,2,3,4]
+	 *   b: [1,2,3,]
+	 *   c: [1]
+	 * }
+	 * 
+ *

+ * 结果是: + * + *

+	 * [
+	 *  {a: 1, b: 1, c: 1}
+	 *  {a: 2, b: 2}
+	 *  {a: 3, b: 3}
+	 *  {a: 4}
+	 * ]
+	 * 
+ * + * @param 键类型 + * @param 值类型 + * @param listMap 列表Map + * @return Map列表 + * @see MapUtil#toMapList(Map) + */ + public static List> toMapList(Map> listMap) { + return MapUtil.toMapList(listMap); + } + + /** + * 集合转换为Map,转换规则为:
+ * 按照keyFunc函数规则根据元素对象生成Key,元素作为值 + * + * @param Map键类型 + * @param Map值类型 + * @param values 数据列表 + * @param map Map对象,转换后的键值对加入此Map,通过传入此对象自定义Map类型 + * @param keyFunc 生成key的函数 + * @return 生成的map + * @since 5.2.6 + */ + public static Map toMap(Iterable values, Map map, Func1 keyFunc) { + return IterUtil.toMap(null == values ? null : values.iterator(), map, keyFunc); + } + + /** + * 集合转换为Map,转换规则为:
+ * 按照keyFunc函数规则根据元素对象生成Key,按照valueFunc函数规则根据元素对象生成value组成新的Map + * + * @param Map键类型 + * @param Map值类型 + * @param 元素类型 + * @param values 数据列表 + * @param map Map对象,转换后的键值对加入此Map,通过传入此对象自定义Map类型 + * @param keyFunc 生成key的函数 + * @param valueFunc 生成值的策略函数 + * @return 生成的map + * @since 5.2.6 + */ + public static Map toMap(Iterable values, Map map, Func1 keyFunc, Func1 valueFunc) { + return IterUtil.toMap(null == values ? null : values.iterator(), map, keyFunc, valueFunc); + } + + /** + * 一个对象不为空且不存在于该集合中时,加入到该集合中
+ *
+	 *     null, null -> false
+	 *     [], null -> false
+	 *     null, "123" -> false
+	 *     ["123"], "123" -> false
+	 *     [], "123" -> true
+	 *     ["456"], "123" -> true
+	 *     [Animal{"name": "jack"}], Dog{"name": "jack"} -> true
+	 * 
+ * + * @param collection 被加入的集合 + * @param object 要添加到集合的对象 + * @param 集合元素类型 + * @param 要添加的元素类型【为集合元素类型的类型或子类型】 + * @return 是否添加成功 + * @author Cloud-Style + */ + public static boolean addIfAbsent(Collection collection, S object) { + if (object == null || collection == null || collection.contains(object)) { + return false; + } + + return collection.add(object); + } + + /** + * 将指定对象全部加入到集合中
+ * 提供的对象如果为集合类型,会自动转换为目标元素类型
+ * + * @param 元素类型 + * @param collection 被加入的集合 + * @param value 对象,可能为Iterator、Iterable、Enumeration、Array + * @return 被加入集合 + */ + public static Collection addAll(Collection collection, Object value) { + return addAll(collection, value, TypeUtil.getTypeArgument(collection.getClass())); + } + + /** + * 将指定对象全部加入到集合中
+ * 提供的对象如果为集合类型,会自动转换为目标元素类型
+ * 如果为String,支持类似于[1,2,3,4] 或者 1,2,3,4 这种格式 + * + * @param 元素类型 + * @param collection 被加入的集合 + * @param value 对象,可能为Iterator、Iterable、Enumeration、Array,或者与集合元素类型一致 + * @param elementType 元素类型,为空时,使用Object类型来接纳所有类型 + * @return 被加入集合 + */ + @SuppressWarnings({"unchecked", "rawtypes"}) + public static Collection addAll(Collection collection, Object value, Type elementType) { + if (null == collection || null == value) { + return collection; + } + if (TypeUtil.isUnknown(elementType)) { + // 元素类型为空时,使用Object类型来接纳所有类型 + elementType = Object.class; + } + + Iterator iter; + if (value instanceof Iterator) { + iter = (Iterator) value; + } else if (value instanceof Iterable) { + iter = ((Iterable) value).iterator(); + } else if (value instanceof Enumeration) { + iter = new EnumerationIter<>((Enumeration) value); + } else if (ArrayUtil.isArray(value)) { + iter = new ArrayIter<>(value); + } else if (value instanceof CharSequence) { + // String按照逗号分隔的列表对待 + final String ArrayStr = StrUtil.unWrap((CharSequence) value, '[', ']'); + iter = StrUtil.splitTrim(ArrayStr, CharUtil.COMMA).iterator(); + } else { + // 其它类型按照单一元素处理 + iter = CollUtil.newArrayList(value).iterator(); + } + + final ConverterRegistry convert = ConverterRegistry.getInstance(); + while (iter.hasNext()) { + collection.add(convert.convert(elementType, iter.next())); + } + + return collection; + } + + /** + * 加入全部 + * + * @param 集合元素类型 + * @param collection 被加入的集合 {@link Collection} + * @param iterator 要加入的{@link Iterator} + * @return 原集合 + */ + public static Collection addAll(Collection collection, Iterator iterator) { + if (null != collection && null != iterator) { + while (iterator.hasNext()) { + collection.add(iterator.next()); + } + } + return collection; + } + + /** + * 加入全部 + * + * @param 集合元素类型 + * @param collection 被加入的集合 {@link Collection} + * @param iterable 要加入的内容{@link Iterable} + * @return 原集合 + */ + public static Collection addAll(Collection collection, Iterable iterable) { + if (iterable == null) { + return collection; + } + return addAll(collection, iterable.iterator()); + } + + /** + * 加入全部 + * + * @param 集合元素类型 + * @param collection 被加入的集合 {@link Collection} + * @param enumeration 要加入的内容{@link Enumeration} + * @return 原集合 + */ + public static Collection addAll(Collection collection, Enumeration enumeration) { + if (null != collection && null != enumeration) { + while (enumeration.hasMoreElements()) { + collection.add(enumeration.nextElement()); + } + } + return collection; + } + + /** + * 加入全部 + * + * @param 集合元素类型 + * @param collection 被加入的集合 {@link Collection} + * @param values 要加入的内容数组 + * @return 原集合 + * @since 3.0.8 + */ + public static Collection addAll(Collection collection, T[] values) { + if (null != collection && null != values) { + Collections.addAll(collection, values); + } + return collection; + } + + /** + * 将另一个列表中的元素加入到列表中,如果列表中已经存在此元素则忽略之 + * + * @param 集合元素类型 + * @param list 列表 + * @param otherList 其它列表 + * @return 此列表 + */ + public static List addAllIfNotContains(List list, List otherList) { + for (T t : otherList) { + if (!list.contains(t)) { + list.add(t); + } + } + return list; + } + + /** + * 获取集合中指定下标的元素值,下标可以为负数,例如-1表示最后一个元素
+ * 如果元素越界,返回null + * + * @param 元素类型 + * @param collection 集合 + * @param index 下标,支持负数 + * @return 元素值 + * @since 4.0.6 + */ + public static T get(Collection collection, int index) { + if (null == collection) { + return null; + } + + final int size = collection.size(); + if (0 == size) { + return null; + } + + if (index < 0) { + index += size; + } + + // 检查越界 + if (index >= size || index < 0) { + return null; + } + + if (collection instanceof List) { + final List list = ((List) collection); + return list.get(index); + } else { + return IterUtil.get(collection.iterator(), index); + } + } + + /** + * 获取集合中指定多个下标的元素值,下标可以为负数,例如-1表示最后一个元素 + * + * @param 元素类型 + * @param collection 集合 + * @param indexes 下标,支持负数 + * @return 元素值列表 + * @since 4.0.6 + */ + @SuppressWarnings("unchecked") + public static List getAny(Collection collection, int... indexes) { + final int size = collection.size(); + final ArrayList result = new ArrayList<>(); + if (collection instanceof List) { + final List list = ((List) collection); + for (int index : indexes) { + if (index < 0) { + index += size; + } + result.add(list.get(index)); + } + } else { + final Object[] array = collection.toArray(); + for (int index : indexes) { + if (index < 0) { + index += size; + } + result.add((T) array[index]); + } + } + return result; + } + + /** + * 获取集合的第一个元素 + * + * @param 集合元素类型 + * @param iterable {@link Iterable} + * @return 第一个元素 + * @see IterUtil#getFirst(Iterable) + * @since 3.0.1 + */ + public static T getFirst(Iterable iterable) { + return IterUtil.getFirst(iterable); + } + + /** + * 获取集合的第一个元素 + * + * @param 集合元素类型 + * @param iterator {@link Iterator} + * @return 第一个元素 + * @see IterUtil#getFirst(Iterator) + * @since 3.0.1 + */ + public static T getFirst(Iterator iterator) { + return IterUtil.getFirst(iterator); + } + + /** + * 获取集合的最后一个元素 + * + * @param 集合元素类型 + * @param collection {@link Collection} + * @return 最后一个元素 + * @since 4.1.10 + */ + public static T getLast(Collection collection) { + return get(collection, -1); + } + + /** + * 获得{@link Iterable}对象的元素类型(通过第一个非空元素判断) + * + * @param iterable {@link Iterable} + * @return 元素类型,当列表为空或元素全部为null时,返回null + * @see IterUtil#getElementType(Iterable) + * @since 3.0.8 + * @deprecated 请使用 {@link IterUtil#getElementType(Iterable)} + */ + @Deprecated + public static Class getElementType(Iterable iterable) { + return IterUtil.getElementType(iterable); + } + + /** + * 获得{@link Iterator}对象的元素类型(通过第一个非空元素判断) + * + * @param iterator {@link Iterator} + * @return 元素类型,当列表为空或元素全部为null时,返回null + * @see IterUtil#getElementType(Iterator) + * @since 3.0.8 + * @deprecated 请使用 {@link IterUtil#getElementType(Iterator)} + */ + @Deprecated + public static Class getElementType(Iterator iterator) { + return IterUtil.getElementType(iterator); + } + + /** + * 从Map中获取指定键列表对应的值列表
+ * 如果key在map中不存在或key对应值为null,则返回值列表对应位置的值也为null + * + * @param 键类型 + * @param 值类型 + * @param map {@link Map} + * @param keys 键列表 + * @return 值列表 + * @since 3.0.8 + */ + @SuppressWarnings("unchecked") + public static ArrayList valuesOfKeys(Map map, K... keys) { + return MapUtil.valuesOfKeys(map, new ArrayIter<>(keys)); + } + + /** + * 从Map中获取指定键列表对应的值列表
+ * 如果key在map中不存在或key对应值为null,则返回值列表对应位置的值也为null + * + * @param 键类型 + * @param 值类型 + * @param map {@link Map} + * @param keys 键列表 + * @return 值列表 + * @since 3.0.9 + */ + public static ArrayList valuesOfKeys(Map map, Iterable keys) { + return valuesOfKeys(map, keys.iterator()); + } + + /** + * 从Map中获取指定键列表对应的值列表
+ * 如果key在map中不存在或key对应值为null,则返回值列表对应位置的值也为null + * + * @param 键类型 + * @param 值类型 + * @param map {@link Map} + * @param keys 键列表 + * @return 值列表 + * @since 3.0.9 + */ + public static ArrayList valuesOfKeys(Map map, Iterator keys) { + return MapUtil.valuesOfKeys(map, keys); + } + + // ------------------------------------------------------------------------------------------------- sort + + /** + * 将多个集合排序并显示不同的段落(分页)
+ * 采用{@link BoundedPriorityQueue}实现分页取局部 + * + * @param 集合元素类型 + * @param pageNo 页码,从0开始计数,0表示第一页 + * @param pageSize 每页的条目数 + * @param comparator 比较器 + * @param colls 集合数组 + * @return 分页后的段落内容 + */ + @SafeVarargs + public static List sortPageAll(int pageNo, int pageSize, Comparator comparator, Collection... colls) { + final List list = new ArrayList<>(pageNo * pageSize); + for (Collection coll : colls) { + list.addAll(coll); + } + if (null != comparator) { + list.sort(comparator); + } + + return page(pageNo, pageSize, list); + } + + /** + * 对指定List分页取值 + * + * @param 集合元素类型 + * @param pageNo 页码,从0开始计数,0表示第一页 + * @param pageSize 每页的条目数 + * @param list 列表 + * @return 分页后的段落内容 + * @since 4.1.20 + */ + public static List page(int pageNo, int pageSize, List list) { + return ListUtil.page(pageNo, pageSize, list); + } + + /** + * 排序集合,排序不会修改原集合 + * + * @param 集合元素类型 + * @param collection 集合 + * @param comparator 比较器 + * @return treeSet + */ + public static List sort(Collection collection, Comparator comparator) { + List list = new ArrayList<>(collection); + list.sort(comparator); + return list; + } + + /** + * 针对List排序,排序会修改原List + * + * @param 元素类型 + * @param list 被排序的List + * @param c {@link Comparator} + * @return 原list + * @see Collections#sort(List, Comparator) + */ + public static List sort(List list, Comparator c) { + return ListUtil.sort(list, c); + } + + /** + * 根据Bean的属性排序 + * + * @param 元素类型 + * @param collection 集合,会被转换为List + * @param property 属性名 + * @return 排序后的List + * @since 4.0.6 + */ + public static List sortByProperty(Collection collection, String property) { + return sort(collection, new PropertyComparator<>(property)); + } + + /** + * 根据Bean的属性排序 + * + * @param 元素类型 + * @param list List + * @param property 属性名 + * @return 排序后的List + * @since 4.0.6 + */ + public static List sortByProperty(List list, String property) { + return ListUtil.sortByProperty(list, property); + } + + /** + * 根据汉字的拼音顺序排序 + * + * @param collection 集合,会被转换为List + * @return 排序后的List + * @since 4.0.8 + */ + public static List sortByPinyin(Collection collection) { + return sort(collection, new PinyinComparator()); + } + + /** + * 根据汉字的拼音顺序排序 + * + * @param list List + * @return 排序后的List + * @since 4.0.8 + */ + public static List sortByPinyin(List list) { + return ListUtil.sortByPinyin(list); + } + + /** + * 排序Map + * + * @param 键类型 + * @param 值类型 + * @param map Map + * @param comparator Entry比较器 + * @return {@link TreeMap} + * @since 3.0.9 + */ + public static TreeMap sort(Map map, Comparator comparator) { + final TreeMap result = new TreeMap<>(comparator); + result.putAll(map); + return result; + } + + /** + * 通过Entry排序,可以按照键排序,也可以按照值排序,亦或者两者综合排序 + * + * @param 键类型 + * @param 值类型 + * @param entryCollection Entry集合 + * @param comparator {@link Comparator} + * @return {@link LinkedList} + * @since 3.0.9 + */ + public static LinkedHashMap sortToMap(Collection> entryCollection, Comparator> comparator) { + List> list = new LinkedList<>(entryCollection); + list.sort(comparator); + + LinkedHashMap result = new LinkedHashMap<>(); + for (Entry entry : list) { + result.put(entry.getKey(), entry.getValue()); + } + return result; + } + + /** + * 通过Entry排序,可以按照键排序,也可以按照值排序,亦或者两者综合排序 + * + * @param 键类型 + * @param 值类型 + * @param map 被排序的Map + * @param comparator {@link Comparator} + * @return {@link LinkedList} + * @since 3.0.9 + */ + public static LinkedHashMap sortByEntry(Map map, Comparator> comparator) { + return sortToMap(map.entrySet(), comparator); + } + + /** + * 将Set排序(根据Entry的值) + * + * @param 键类型 + * @param 值类型 + * @param collection 被排序的{@link Collection} + * @return 排序后的Set + */ + @SuppressWarnings({"unchecked", "rawtypes"}) + public static List> sortEntryToList(Collection> collection) { + List> list = new LinkedList<>(collection); + list.sort((o1, o2) -> { + V v1 = o1.getValue(); + V v2 = o2.getValue(); + + if (v1 instanceof Comparable) { + return ((Comparable) v1).compareTo(v2); + } else { + return v1.toString().compareTo(v2.toString()); + } + }); + return list; + } + + // ------------------------------------------------------------------------------------------------- forEach + + /** + * 循环遍历 {@link Iterable},使用{@link Consumer} 接受遍历的每条数据,并针对每条数据做处理 + * + * @param 集合元素类型 + * @param iterable {@link Iterable} + * @param consumer {@link Consumer} 遍历的每条数据处理器 + * @since 5.4.7 + */ + public static void forEach(Iterable iterable, Consumer consumer) { + if (iterable == null) { + return; + } + forEach(iterable.iterator(), consumer); + } + + /** + * 循环遍历 {@link Iterator},使用{@link Consumer} 接受遍历的每条数据,并针对每条数据做处理 + * + * @param 集合元素类型 + * @param iterator {@link Iterator} + * @param consumer {@link Consumer} 遍历的每条数据处理器 + */ + public static void forEach(Iterator iterator, Consumer consumer) { + if (iterator == null) { + return; + } + int index = 0; + while (iterator.hasNext()) { + consumer.accept(iterator.next(), index); + index++; + } + } + + /** + * 循环遍历 {@link Enumeration},使用{@link Consumer} 接受遍历的每条数据,并针对每条数据做处理 + * + * @param 集合元素类型 + * @param enumeration {@link Enumeration} + * @param consumer {@link Consumer} 遍历的每条数据处理器 + */ + public static void forEach(Enumeration enumeration, Consumer consumer) { + if (enumeration == null) { + return; + } + int index = 0; + while (enumeration.hasMoreElements()) { + consumer.accept(enumeration.nextElement(), index); + index++; + } + } + + /** + * 循环遍历Map,使用{@link KVConsumer} 接受遍历的每条数据,并针对每条数据做处理
+ * 和JDK8中的map.forEach不同的是,此方法支持index + * + * @param Key类型 + * @param Value类型 + * @param map {@link Map} + * @param kvConsumer {@link KVConsumer} 遍历的每条数据处理器 + */ + public static void forEach(Map map, KVConsumer kvConsumer) { + if (map == null) { + return; + } + int index = 0; + for (Entry entry : map.entrySet()) { + kvConsumer.accept(entry.getKey(), entry.getValue(), index); + index++; + } + } + + /** + * 分组,按照{@link Hash32}接口定义的hash算法,集合中的元素放入hash值对应的子列表中 + * + * @param 元素类型 + * @param collection 被分组的集合 + * @param hash Hash值算法,决定元素放在第几个分组的规则 + * @return 分组后的集合 + */ + public static List> group(Collection collection, Hash32 hash) { + final List> result = new ArrayList<>(); + if (isEmpty(collection)) { + return result; + } + if (null == hash) { + // 默认hash算法,按照元素的hashCode分组 + hash = t -> (null == t) ? 0 : t.hashCode(); + } + + int index; + List subList; + for (T t : collection) { + index = hash.hash32(t); + if (result.size() - 1 < index) { + while (result.size() - 1 < index) { + result.add(null); + } + result.set(index, newArrayList(t)); + } else { + subList = result.get(index); + if (null == subList) { + result.set(index, newArrayList(t)); + } else { + subList.add(t); + } + } + } + return result; + } + + /** + * 根据元素的指定字段名分组,非Bean都放在第一个分组中 + * + * @param 元素类型 + * @param collection 集合 + * @param fieldName 元素Bean中的字段名,非Bean都放在第一个分组中 + * @return 分组列表 + */ + public static List> groupByField(Collection collection, final String fieldName) { + return group(collection, new Hash32() { + private final List fieldNameList = new ArrayList<>(); + + @Override + public int hash32(T t) { + if (null == t || !BeanUtil.isBean(t.getClass())) { + // 非Bean放在同一子分组中 + return 0; + } + final Object value = ReflectUtil.getFieldValue(t, fieldName); + int hash = fieldNameList.indexOf(value); + if (hash < 0) { + fieldNameList.add(value); + return fieldNameList.size() - 1; + } else { + return hash; + } + } + }); + } + + /** + * 反序给定List,会在原List基础上直接修改 + * + * @param 元素类型 + * @param list 被反转的List + * @return 反转后的List + * @since 4.0.6 + */ + public static List reverse(List list) { + return ListUtil.reverse(list); + } + + /** + * 反序给定List,会创建一个新的List,原List数据不变 + * + * @param 元素类型 + * @param list 被反转的List + * @return 反转后的List + * @since 4.0.6 + */ + public static List reverseNew(List list) { + return ListUtil.reverseNew(list); + } + + /** + * 设置或增加元素。当index小于List的长度时,替换指定位置的值,否则在尾部追加 + * + * @param 元素类型 + * @param list List列表 + * @param index 位置 + * @param element 新元素 + * @return 原List + * @since 4.1.2 + */ + public static List setOrAppend(List list, int index, T element) { + return ListUtil.setOrAppend(list, index, element); + } + + /** + * 获取指定Map列表中所有的Key + * + * @param 键类型 + * @param mapCollection Map列表 + * @return key集合 + * @since 4.5.12 + */ + public static Set keySet(Collection> mapCollection) { + if (isEmpty(mapCollection)) { + return new HashSet<>(); + } + final HashSet set = new HashSet<>(mapCollection.size() * 16); + for (Map map : mapCollection) { + set.addAll(map.keySet()); + } + + return set; + } + + /** + * 获取指定Map列表中所有的Value + * + * @param 值类型 + * @param mapCollection Map列表 + * @return Value集合 + * @since 4.5.12 + */ + public static List values(Collection> mapCollection) { + final List values = new ArrayList<>(); + for (Map map : mapCollection) { + values.addAll(map.values()); + } + + return values; + } + + /** + * 取最大值 + * + * @param 元素类型 + * @param coll 集合 + * @return 最大值 + * @see Collections#max(Collection) + * @since 4.6.5 + */ + public static > T max(Collection coll) { + return Collections.max(coll); + } + + /** + * 取最小值 + * + * @param 元素类型 + * @param coll 集合 + * @return 最小值 + * @see Collections#min(Collection) + * @since 4.6.5 + */ + public static > T min(Collection coll) { + return Collections.min(coll); + } + + /** + * 转为只读集合 + * + * @param 元素类型 + * @param c 集合 + * @return 只读集合 + * @since 5.2.6 + */ + public static Collection unmodifiable(Collection c) { + return Collections.unmodifiableCollection(c); + } + + /** + * 根据给定的集合类型,返回对应的空集合,支持类型包括: + * * + *
+	 *     1. NavigableSet
+	 *     2. SortedSet
+	 *     3. Set
+	 *     4. List
+	 * 
+ * + * @param 元素类型 + * @param 集合类型 + * @param collectionClass 集合类型 + * @return 空集合 + * @since 5.3.1 + */ + @SuppressWarnings("unchecked") + public static > T empty(Class collectionClass) { + if (null == collectionClass) { + return (T) Collections.emptyList(); + } + + if (Set.class.isAssignableFrom(collectionClass)) { + if (NavigableSet.class == collectionClass) { + return (T) Collections.emptyNavigableSet(); + } else if (SortedSet.class == collectionClass) { + return (T) Collections.emptySortedSet(); + } else { + return (T) Collections.emptySet(); + } + } else if (List.class.isAssignableFrom(collectionClass)) { + return (T) Collections.emptyList(); + } + + // 不支持空集合的集合类型 + throw new IllegalArgumentException(StrUtil.format("[{}] is not support to get empty!", collectionClass)); + } + + /** + * 清除一个或多个集合内的元素,每个集合调用clear()方法 + * + * @param collections 一个或多个集合 + * @since 5.3.6 + */ + public static void clear(Collection... collections) { + for (Collection collection : collections) { + if (isNotEmpty(collection)) { + collection.clear(); + } + } + } + + /** + * 填充List,以达到最小长度 + * + * @param 集合元素类型 + * @param list 列表 + * @param minLen 最小长度 + * @param padObj 填充的对象 + * @since 5.3.10 + */ + public static void padLeft(List list, int minLen, T padObj) { + Objects.requireNonNull(list); + if (list.isEmpty()) { + padRight(list, minLen, padObj); + return; + } + for (int i = list.size(); i < minLen; i++) { + list.add(0, padObj); + } + } + + /** + * 填充List,以达到最小长度 + * + * @param 集合元素类型 + * @param list 列表 + * @param minLen 最小长度 + * @param padObj 填充的对象 + * @since 5.3.10 + */ + public static void padRight(Collection list, int minLen, T padObj) { + Objects.requireNonNull(list); + for (int i = list.size(); i < minLen; i++) { + list.add(padObj); + } + } + + /** + * 使用给定的转换函数,转换源集合为新类型的集合 + * + * @param 源元素类型 + * @param 目标元素类型 + * @param collection 集合 + * @param function 转换函数 + * @return 新类型的集合 + * @since 5.4.3 + */ + public static Collection trans(Collection collection, Function function) { + return new TransCollection<>(collection, function); + } + + /** + * 使用给定的map将集合中的原素进行属性或者值的重新设定 + * + * @param 元素类型 + * @param 替换的键 + * @param 替换的值 + * @param iterable 集合 + * @param map 映射集 + * @param keyGenerate 映射键生成函数 + * @param biConsumer 封装映射到的值函数 + * @author nick_wys + * @since 5.7.18 + */ + public static void setValueByMap(Iterable iterable, Map map, Function keyGenerate, BiConsumer biConsumer) { + iterable.forEach(x -> Optional.ofNullable(map.get(keyGenerate.apply(x))).ifPresent(y -> biConsumer.accept(x, y))); + } + + // ---------------------------------------------------------------------------------------------- Interface start + + /** + * 针对一个参数做相应的操作
+ * 此函数接口与JDK8中Consumer不同是多提供了index参数,用于标记遍历对象是第几个。 + * + * @param 处理参数类型 + * @author Looly + */ + @FunctionalInterface + public interface Consumer extends Serializable { + /** + * 接受并处理一个参数 + * + * @param value 参数值 + * @param index 参数在集合中的索引 + */ + void accept(T value, int index); + } + + /** + * 针对两个参数做相应的操作,例如Map中的KEY和VALUE + * + * @param KEY类型 + * @param VALUE类型 + * @author Looly + */ + @FunctionalInterface + public interface KVConsumer extends Serializable { + /** + * 接受并处理一对参数 + * + * @param key 键 + * @param value 值 + * @param index 参数在集合中的索引 + */ + void accept(K key, V value, int index); + } + // ---------------------------------------------------------------------------------------------- Interface end + + /** + * 获取Collection或者iterator的大小,此方法可以处理的对象类型如下: + *
    + *
  • Collection - the collection size + *
  • Map - the map size + *
  • Array - the array size + *
  • Iterator - the number of elements remaining in the iterator + *
  • Enumeration - the number of elements remaining in the enumeration + *
+ * + * @param object 可以为空的对象 + * @return 如果object为空则返回0 + * @throws IllegalArgumentException 参数object不是Collection或者iterator + * @since 5.5.0 + */ + public static int size(final Object object) { + if (object == null) { + return 0; + } + + int total = 0; + if (object instanceof Map) { + total = ((Map) object).size(); + } else if (object instanceof Collection) { + total = ((Collection) object).size(); + } else if (object instanceof Iterable) { + total = IterUtil.size((Iterable) object); + } else if (object instanceof Iterator) { + total = IterUtil.size((Iterator) object); + } else if (object instanceof Enumeration) { + final Enumeration it = (Enumeration) object; + while (it.hasMoreElements()) { + total++; + it.nextElement(); + } + } else if (ArrayUtil.isArray(object)) { + total = ArrayUtil.length(object); + } else { + throw new IllegalArgumentException("Unsupported object type: " + object.getClass().getName()); + } + return total; + } + + /** + * 判断两个{@link Collection} 是否元素和顺序相同,返回{@code true}的条件是: + *
    + *
  • 两个{@link Collection}必须长度相同
  • + *
  • 两个{@link Collection}元素相同index的对象必须equals,满足{@link Objects#equals(Object, Object)}
  • + *
+ * 此方法来自Apache-Commons-Collections4。 + * + * @param list1 列表1 + * @param list2 列表2 + * @return 是否相同 + * @since 5.6.0 + */ + public static boolean isEqualList(final Collection list1, final Collection list2) { + if (list1 == list2) { + return true; + } + if (list1 == null || list2 == null || list1.size() != list2.size()) { + return false; + } + + return IterUtil.isEqualList(list1, list2); + } +} diff --git a/src/main/java/cn/hutool/core/collection/CollectionUtil.java b/src/main/java/cn/hutool/core/collection/CollectionUtil.java new file mode 100644 index 0000000..66d89dc --- /dev/null +++ b/src/main/java/cn/hutool/core/collection/CollectionUtil.java @@ -0,0 +1,10 @@ +package cn.hutool.core.collection; + +/** + * 集合相关工具类,包括数组,是 {@link CollUtil} 的别名工具类 + * + * @author xiaoleilu + * @see CollUtil + */ +public class CollectionUtil extends CollUtil{ +} diff --git a/src/main/java/cn/hutool/core/collection/ComputeIter.java b/src/main/java/cn/hutool/core/collection/ComputeIter.java new file mode 100644 index 0000000..7ddff40 --- /dev/null +++ b/src/main/java/cn/hutool/core/collection/ComputeIter.java @@ -0,0 +1,73 @@ +package cn.hutool.core.collection; + +import java.util.Iterator; +import java.util.NoSuchElementException; + +/** + * 带有计算属性的遍历器
+ * 通过继承此抽象遍历器,实现{@link #computeNext()}计算下一个节点,即可完成节点遍历
+ * 当调用{@link #hasNext()}时将此方法产生的节点缓存,直到调用{@link #next()}取出
+ * 当无下一个节点时,须返回{@code null}表示遍历结束 + * + * @param 节点类型 + * @author looly + * @since 5.7.14 + */ +public abstract class ComputeIter implements Iterator { + + private T next; + /** + * A flag indicating if the iterator has been fully read. + */ + private boolean finished; + + /** + * 计算新的节点,通过实现此方法,当调用{@link #hasNext()}时将此方法产生的节点缓存,直到调用{@link #next()}取出
+ * 当无下一个节点时,须返回{@code null}表示遍历结束 + * + * @return 节点值 + */ + protected abstract T computeNext(); + + @Override + public boolean hasNext() { + if (null != next) { + // 用户读取了节点,但是没有使用 + return true; + } else if (finished) { + // 读取结束 + return false; + } + + T result = computeNext(); + if (null == result) { + // 不再有新的节点,结束 + this.finished = true; + return false; + } else { + this.next = result; + return true; + } + + } + + @Override + public T next() { + if (!hasNext()) { + throw new NoSuchElementException("No more lines"); + } + + T result = this.next; + // 清空cache,表示此节点读取完毕,下次计算新节点 + this.next = null; + return result; + } + + /** + * 手动结束遍历器,用于关闭操作等 + */ + public void finish(){ + this.finished = true; + this.next = null; + } +} diff --git a/src/main/java/cn/hutool/core/collection/ConcurrentHashSet.java b/src/main/java/cn/hutool/core/collection/ConcurrentHashSet.java new file mode 100644 index 0000000..bac6e1b --- /dev/null +++ b/src/main/java/cn/hutool/core/collection/ConcurrentHashSet.java @@ -0,0 +1,117 @@ +package cn.hutool.core.collection; + +import cn.hutool.core.map.SafeConcurrentHashMap; + +import java.util.AbstractSet; +import java.util.Collection; +import java.util.Iterator; + +/** + * 通过{@link SafeConcurrentHashMap}实现的线程安全HashSet + * + * @author Looly + * + * @param 元素类型 + * @since 3.1.0 + */ +public class ConcurrentHashSet extends AbstractSet implements java.io.Serializable { + private static final long serialVersionUID = 7997886765361607470L; + + /** 持有对象。如果值为此对象表示有数据,否则无数据 */ + private static final Boolean PRESENT = true; + private final SafeConcurrentHashMap map; + + // ----------------------------------------------------------------------------------- Constructor start + /** + * 构造
+ * 触发因子为默认的0.75 + */ + public ConcurrentHashSet() { + map = new SafeConcurrentHashMap<>(); + } + + /** + * 构造
+ * 触发因子为默认的0.75 + * + * @param initialCapacity 初始大小 + */ + public ConcurrentHashSet(int initialCapacity) { + map = new SafeConcurrentHashMap<>(initialCapacity); + } + + /** + * 构造 + * + * @param initialCapacity 初始大小 + * @param loadFactor 加载因子。此参数决定数据增长时触发的百分比 + */ + public ConcurrentHashSet(int initialCapacity, float loadFactor) { + map = new SafeConcurrentHashMap<>(initialCapacity, loadFactor); + } + + /** + * 构造 + * + * @param initialCapacity 初始大小 + * @param loadFactor 触发因子。此参数决定数据增长时触发的百分比 + * @param concurrencyLevel 线程并发度 + */ + public ConcurrentHashSet(int initialCapacity, float loadFactor, int concurrencyLevel) { + map = new SafeConcurrentHashMap<>(initialCapacity, loadFactor, concurrencyLevel); + } + + /** + * 从已有集合中构造 + * @param iter {@link Iterable} + */ + public ConcurrentHashSet(Iterable iter) { + if(iter instanceof Collection) { + final Collection collection = (Collection)iter; + map = new SafeConcurrentHashMap<>((int)(collection.size() / 0.75f)); + this.addAll(collection); + }else { + map = new SafeConcurrentHashMap<>(); + for (E e : iter) { + this.add(e); + } + } + } + // ----------------------------------------------------------------------------------- Constructor end + + @Override + public Iterator iterator() { + return map.keySet().iterator(); + } + + @Override + public int size() { + return map.size(); + } + + @Override + public boolean isEmpty() { + return map.isEmpty(); + } + + @Override + public boolean contains(Object o) { + //noinspection SuspiciousMethodCalls + return map.containsKey(o); + } + + @Override + public boolean add(E e) { + return map.put(e, PRESENT) == null; + } + + @Override + public boolean remove(Object o) { + return PRESENT.equals(map.remove(o)); + } + + @Override + public void clear() { + map.clear(); + } +} diff --git a/src/main/java/cn/hutool/core/collection/CopiedIter.java b/src/main/java/cn/hutool/core/collection/CopiedIter.java new file mode 100644 index 0000000..568f344 --- /dev/null +++ b/src/main/java/cn/hutool/core/collection/CopiedIter.java @@ -0,0 +1,68 @@ +package cn.hutool.core.collection; + +import java.io.Serializable; +import java.util.Iterator; +import java.util.List; + +/** + * 复制 {@link Iterator}
+ * 为了解决并发情况下{@link Iterator}遍历导致的问题(当Iterator被修改会抛出ConcurrentModificationException) + * ,故使用复制原Iterator的方式解决此问题。 + * + *

+ * 解决方法为:在构造方法中遍历Iterator中的元素,装入新的List中然后遍历之。 + * 当然,修改这个复制后的Iterator是没有意义的,因此remove方法将会抛出异常。 + * + *

+ * 需要注意的是,在构造此对象时需要保证原子性(原对象不被修改),最好加锁构造此对象,构造完毕后解锁。 + * + * @param 元素类型 + * @author Looly + * @since 3.0.7 + */ +public class CopiedIter implements IterableIter, Serializable { + private static final long serialVersionUID = 1L; + + private final Iterator listIterator; + + /** + * 根据已有{@link Iterator},返回新的{@code CopiedIter} + * + * @param iterator {@link Iterator} + * @param 元素类型 + * @return {@code CopiedIter} + */ + public static CopiedIter copyOf(Iterator iterator) { + return new CopiedIter<>(iterator); + } + + /** + * 构造 + * + * @param iterator 被复制的Iterator + */ + public CopiedIter(Iterator iterator) { + final List eleList = ListUtil.toList(iterator); + this.listIterator = eleList.iterator(); + } + + @Override + public boolean hasNext() { + return this.listIterator.hasNext(); + } + + @Override + public E next() { + return this.listIterator.next(); + } + + /** + * 此对象不支持移除元素 + * + * @throws UnsupportedOperationException 当调用此方法时始终抛出此异常 + */ + @Override + public void remove() throws UnsupportedOperationException { + throw new UnsupportedOperationException("This is a read-only iterator."); + } +} diff --git a/src/main/java/cn/hutool/core/collection/EnumerationIter.java b/src/main/java/cn/hutool/core/collection/EnumerationIter.java new file mode 100644 index 0000000..3974a1b --- /dev/null +++ b/src/main/java/cn/hutool/core/collection/EnumerationIter.java @@ -0,0 +1,41 @@ +package cn.hutool.core.collection; + +import java.io.Serializable; +import java.util.Enumeration; +import java.util.Iterator; + +/** + * {@link Enumeration}对象转{@link Iterator}对象 + * @author Looly + * + * @param 元素类型 + * @since 4.1.1 + */ +public class EnumerationIter implements IterableIter, Serializable{ + private static final long serialVersionUID = 1L; + + private final Enumeration e; + + /** + * 构造 + * @param enumeration {@link Enumeration}对象 + */ + public EnumerationIter(Enumeration enumeration) { + this.e = enumeration; + } + + @Override + public boolean hasNext() { + return e.hasMoreElements(); + } + + @Override + public E next() { + return e.nextElement(); + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } +} diff --git a/src/main/java/cn/hutool/core/collection/FilterIter.java b/src/main/java/cn/hutool/core/collection/FilterIter.java new file mode 100644 index 0000000..9495be7 --- /dev/null +++ b/src/main/java/cn/hutool/core/collection/FilterIter.java @@ -0,0 +1,96 @@ +package cn.hutool.core.collection; + +import cn.hutool.core.lang.Assert; +import cn.hutool.core.lang.Filter; + +import java.util.Iterator; +import java.util.NoSuchElementException; + +/** + * 包装 {@link Iterator}并根据{@link Filter}定义,过滤元素输出
+ * 类实现来自Apache Commons Collection + * + * @author apache commons, looly + * @since 5.8.0 + */ +public class FilterIter implements Iterator { + + private final Iterator iterator; + private final Filter filter; + + /** + * 下一个元素 + */ + private E nextObject; + /** + * 标记下一个元素是否被计算 + */ + private boolean nextObjectSet = false; + + /** + * 构造 + * + * @param iterator 被包装的{@link Iterator} + * @param filter 过滤函数,{@code null}表示不过滤 + */ + public FilterIter(final Iterator iterator, final Filter filter) { + this.iterator = Assert.notNull(iterator); + this.filter = filter; + } + + @Override + public boolean hasNext() { + return nextObjectSet || setNextObject(); + } + + @Override + public E next() { + if (!nextObjectSet && !setNextObject()) { + throw new NoSuchElementException(); + } + nextObjectSet = false; + return nextObject; + } + + @Override + public void remove() { + if (nextObjectSet) { + throw new IllegalStateException("remove() cannot be called"); + } + iterator.remove(); + } + + /** + * 获取被包装的{@link Iterator} + * + * @return {@link Iterator} + */ + public Iterator getIterator() { + return iterator; + } + + /** + * 获取过滤函数 + * + * @return 过滤函数,可能为{@code null} + */ + public Filter getFilter() { + return filter; + } + + /** + * 设置下一个元素,如果存在返回{@code true},否则{@code false} + */ + private boolean setNextObject() { + while (iterator.hasNext()) { + final E object = iterator.next(); + if (null == filter || filter.accept(object)) { + nextObject = object; + nextObjectSet = true; + return true; + } + } + return false; + } + +} diff --git a/src/main/java/cn/hutool/core/collection/IterChain.java b/src/main/java/cn/hutool/core/collection/IterChain.java new file mode 100644 index 0000000..61ea65d --- /dev/null +++ b/src/main/java/cn/hutool/core/collection/IterChain.java @@ -0,0 +1,91 @@ +package cn.hutool.core.collection; + +import cn.hutool.core.lang.Chain; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.NoSuchElementException; + +/** + * 组合{@link Iterator},将多个{@link Iterator}组合在一起,便于集中遍历。
+ * 来自Jodd + * + * @param 元素类型 + * @author looly, jodd + */ +public class IterChain implements Iterator, Chain, IterChain> { + + protected final List> allIterators = new ArrayList<>(); + + /** + * 构造 + * 可以使用 {@link #addChain(Iterator)} 方法加入更多的集合。 + */ + public IterChain() { + } + + /** + * 构造 + * @param iterators 多个{@link Iterator} + */ + @SafeVarargs + public IterChain(Iterator... iterators) { + for (final Iterator iterator : iterators) { + addChain(iterator); + } + } + + @Override + public IterChain addChain(Iterator iterator) { + if (allIterators.contains(iterator)) { + throw new IllegalArgumentException("Duplicate iterator"); + } + allIterators.add(iterator); + return this; + } + + // ---------------------------------------------------------------- interface + + protected int currentIter = -1; + + @Override + public boolean hasNext() { + if (currentIter == -1) { + currentIter = 0; + } + + final int size = allIterators.size(); + for (int i = currentIter; i < size; i++) { + final Iterator iterator = allIterators.get(i); + if (iterator.hasNext()) { + currentIter = i; + return true; + } + } + return false; + } + + @Override + public T next() { + if (!hasNext()) { + throw new NoSuchElementException(); + } + + return allIterators.get(currentIter).next(); + } + + @Override + public void remove() { + if (-1 == currentIter) { + throw new IllegalStateException("next() has not yet been called"); + } + + allIterators.get(currentIter).remove(); + } + + @Override + public Iterator> iterator() { + return this.allIterators.iterator(); + } +} diff --git a/src/main/java/cn/hutool/core/collection/IterUtil.java b/src/main/java/cn/hutool/core/collection/IterUtil.java new file mode 100644 index 0000000..a79e17f --- /dev/null +++ b/src/main/java/cn/hutool/core/collection/IterUtil.java @@ -0,0 +1,1064 @@ +package cn.hutool.core.collection; + +import cn.hutool.core.exceptions.UtilException; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.lang.Editor; +import cn.hutool.core.lang.Filter; +import cn.hutool.core.lang.Matcher; +import cn.hutool.core.lang.func.Func1; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.text.StrJoiner; +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.ReflectUtil; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Dictionary; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Objects; +import java.util.function.Consumer; +import java.util.function.Function; + +/** + * {@link Iterable} 和 {@link Iterator} 相关工具类 + * + * @author Looly + * @since 3.1.0 + */ +public class IterUtil { + + /** + * 获取{@link Iterator} + * + * @param iterable {@link Iterable} + * @param 元素类型 + * @return 当iterable为null返回{@code null},否则返回对应的{@link Iterator} + * @since 5.7.2 + */ + public static Iterator getIter(Iterable iterable) { + return null == iterable ? null : iterable.iterator(); + } + + /** + * Iterable是否为空 + * + * @param iterable Iterable对象 + * @return 是否为空 + */ + public static boolean isEmpty(Iterable iterable) { + return null == iterable || isEmpty(iterable.iterator()); + } + + /** + * Iterator是否为空 + * + * @param Iterator Iterator对象 + * @return 是否为空 + */ + public static boolean isEmpty(Iterator Iterator) { + return null == Iterator || !Iterator.hasNext(); + } + + /** + * Iterable是否为空 + * + * @param iterable Iterable对象 + * @return 是否为空 + */ + public static boolean isNotEmpty(Iterable iterable) { + return null != iterable && isNotEmpty(iterable.iterator()); + } + + /** + * Iterator是否为空 + * + * @param Iterator Iterator对象 + * @return 是否为空 + */ + public static boolean isNotEmpty(Iterator Iterator) { + return null != Iterator && Iterator.hasNext(); + } + + /** + * 是否包含{@code null}元素 + * + * @param iter 被检查的{@link Iterable}对象,如果为{@code null} 返回true + * @return 是否包含{@code null}元素 + */ + public static boolean hasNull(Iterable iter) { + return hasNull(null == iter ? null : iter.iterator()); + } + + /** + * 是否包含{@code null}元素 + * + * @param iter 被检查的{@link Iterator}对象,如果为{@code null} 返回true + * @return 是否包含{@code null}元素 + */ + public static boolean hasNull(Iterator iter) { + if (null == iter) { + return true; + } + while (iter.hasNext()) { + if (null == iter.next()) { + return true; + } + } + + return false; + } + + /** + * 是否全部元素为null + * + * @param iter iter 被检查的{@link Iterable}对象,如果为{@code null} 返回true + * @return 是否全部元素为null + * @since 3.3.0 + */ + public static boolean isAllNull(Iterable iter) { + return isAllNull(null == iter ? null : iter.iterator()); + } + + /** + * 是否全部元素为null + * + * @param iter iter 被检查的{@link Iterator}对象,如果为{@code null} 返回true + * @return 是否全部元素为null + * @since 3.3.0 + */ + public static boolean isAllNull(Iterator iter) { + return null == getFirstNoneNull(iter); + } + + /** + * 根据集合返回一个元素计数的 {@link Map}
+ * 所谓元素计数就是假如这个集合中某个元素出现了n次,那将这个元素做为key,n做为value
+ * 例如:[a,b,c,c,c] 得到:
+ * a: 1
+ * b: 1
+ * c: 3
+ * + * @param 集合元素类型 + * @param iter {@link Iterator},如果为null返回一个空的Map + * @return {@link Map} + */ + public static Map countMap(Iterator iter) { + final HashMap countMap = new HashMap<>(); + if (null != iter) { + T t; + while (iter.hasNext()) { + t = iter.next(); + countMap.put(t, countMap.getOrDefault(t, 0) + 1); + } + } + return countMap; + } + + /** + * 字段值与列表值对应的Map,常用于元素对象中有唯一ID时需要按照这个ID查找对象的情况
+ * 例如:车牌号 =》车 + * + * @param 字段名对应值得类型,不确定请使用Object + * @param 对象类型 + * @param iter 对象列表 + * @param fieldName 字段名(会通过反射获取其值) + * @return 某个字段值与对象对应Map + * @since 4.0.4 + */ + @SuppressWarnings("unchecked") + public static Map fieldValueMap(Iterator iter, String fieldName) { + return toMap(iter, new HashMap<>(), (value) -> (K) ReflectUtil.getFieldValue(value, fieldName)); + } + + /** + * 两个字段值组成新的Map + * + * @param 字段名对应值得类型,不确定请使用Object + * @param 值类型,不确定使用Object + * @param iter 对象列表 + * @param fieldNameForKey 做为键的字段名(会通过反射获取其值) + * @param fieldNameForValue 做为值的字段名(会通过反射获取其值) + * @return 某个字段值与对象对应Map + * @since 4.0.10 + */ + @SuppressWarnings("unchecked") + public static Map fieldValueAsMap(Iterator iter, String fieldNameForKey, String fieldNameForValue) { + return toMap(iter, new HashMap<>(), + (value) -> (K) ReflectUtil.getFieldValue(value, fieldNameForKey), + (value) -> (V) ReflectUtil.getFieldValue(value, fieldNameForValue) + ); + } + + /** + * 获取指定Bean列表中某个字段,生成新的列表 + * + * @param 对象类型 + * @param iterable 对象列表 + * @param fieldName 字段名(会通过反射获取其值) + * @return 某个字段值与对象对应Map + * @since 4.6.2 + */ + public static List fieldValueList(Iterable iterable, String fieldName) { + return fieldValueList(getIter(iterable), fieldName); + } + + /** + * 获取指定Bean列表中某个字段,生成新的列表 + * + * @param 对象类型 + * @param iter 对象列表 + * @param fieldName 字段名(会通过反射获取其值) + * @return 某个字段值与对象对应Map + * @since 4.0.10 + */ + public static List fieldValueList(Iterator iter, String fieldName) { + final List result = new ArrayList<>(); + if (null != iter) { + V value; + while (iter.hasNext()) { + value = iter.next(); + result.add(ReflectUtil.getFieldValue(value, fieldName)); + } + } + return result; + } + + /** + * 以 conjunction 为分隔符将集合转换为字符串
+ * 如果集合元素为数组、{@link Iterable}或{@link Iterator},则递归组合其为字符串 + * + * @param 集合元素类型 + * @param iterator 集合 + * @param conjunction 分隔符 + * @return 连接后的字符串 + */ + public static String join(Iterator iterator, CharSequence conjunction) { + return StrJoiner.of(conjunction).append(iterator).toString(); + } + + /** + * 以 conjunction 为分隔符将集合转换为字符串
+ * 如果集合元素为数组、{@link Iterable}或{@link Iterator},则递归组合其为字符串 + * + * @param 集合元素类型 + * @param iterator 集合 + * @param conjunction 分隔符 + * @param prefix 每个元素添加的前缀,null表示不添加 + * @param suffix 每个元素添加的后缀,null表示不添加 + * @return 连接后的字符串 + * @since 4.0.10 + */ + public static String join(Iterator iterator, CharSequence conjunction, String prefix, String suffix) { + return StrJoiner.of(conjunction, prefix, suffix) + // 每个元素都添加前后缀 + .setWrapElement(true) + .append(iterator) + .toString(); + } + + /** + * 以 conjunction 为分隔符将集合转换为字符串
+ * 如果集合元素为数组、{@link Iterable}或{@link Iterator},则递归组合其为字符串 + * + * @param 集合元素类型 + * @param iterator 集合 + * @param conjunction 分隔符 + * @param func 集合元素转换器,将元素转换为字符串 + * @return 连接后的字符串 + * @since 5.6.7 + */ + public static String join(Iterator iterator, CharSequence conjunction, Function func) { + if (null == iterator) { + return null; + } + + return StrJoiner.of(conjunction).append(iterator, func).toString(); + } + + /** + * 将Entry集合转换为HashMap + * + * @param 键类型 + * @param 值类型 + * @param entryIter entry集合 + * @return Map + */ + public static HashMap toMap(Iterable> entryIter) { + final HashMap map = new HashMap<>(); + if (isNotEmpty(entryIter)) { + for (Entry entry : entryIter) { + map.put(entry.getKey(), entry.getValue()); + } + } + return map; + } + + /** + * 将键列表和值列表转换为Map
+ * 以键为准,值与键位置需对应。如果键元素数多于值元素,多余部分值用null代替。
+ * 如果值多于键,忽略多余的值。 + * + * @param 键类型 + * @param 值类型 + * @param keys 键列表 + * @param values 值列表 + * @return 标题内容Map + * @since 3.1.0 + */ + public static Map toMap(Iterable keys, Iterable values) { + return toMap(keys, values, false); + } + + /** + * 将键列表和值列表转换为Map
+ * 以键为准,值与键位置需对应。如果键元素数多于值元素,多余部分值用null代替。
+ * 如果值多于键,忽略多余的值。 + * + * @param 键类型 + * @param 值类型 + * @param keys 键列表 + * @param values 值列表 + * @param isOrder 是否有序 + * @return 标题内容Map + * @since 4.1.12 + */ + public static Map toMap(Iterable keys, Iterable values, boolean isOrder) { + return toMap(null == keys ? null : keys.iterator(), null == values ? null : values.iterator(), isOrder); + } + + /** + * 将键列表和值列表转换为Map
+ * 以键为准,值与键位置需对应。如果键元素数多于值元素,多余部分值用null代替。
+ * 如果值多于键,忽略多余的值。 + * + * @param 键类型 + * @param 值类型 + * @param keys 键列表 + * @param values 值列表 + * @return 标题内容Map + * @since 3.1.0 + */ + public static Map toMap(Iterator keys, Iterator values) { + return toMap(keys, values, false); + } + + /** + * 将键列表和值列表转换为Map
+ * 以键为准,值与键位置需对应。如果键元素数多于值元素,多余部分值用null代替。
+ * 如果值多于键,忽略多余的值。 + * + * @param 键类型 + * @param 值类型 + * @param keys 键列表 + * @param values 值列表 + * @param isOrder 是否有序 + * @return 标题内容Map + * @since 4.1.12 + */ + public static Map toMap(Iterator keys, Iterator values, boolean isOrder) { + final Map resultMap = MapUtil.newHashMap(isOrder); + if (isNotEmpty(keys)) { + while (keys.hasNext()) { + resultMap.put(keys.next(), (null != values && values.hasNext()) ? values.next() : null); + } + } + return resultMap; + } + + /** + * 将列表转成值为List的HashMap + * + * @param iterable 值列表 + * @param keyMapper Map的键映射 + * @param 键类型 + * @param 值类型 + * @return HashMap + * @since 5.3.6 + */ + public static Map> toListMap(Iterable iterable, Function keyMapper) { + return toListMap(iterable, keyMapper, v -> v); + } + + /** + * 将列表转成值为List的HashMap + * + * @param iterable 值列表 + * @param keyMapper Map的键映射 + * @param valueMapper Map中List的值映射 + * @param 列表值类型 + * @param 键类型 + * @param 值类型 + * @return HashMap + * @since 5.3.6 + */ + public static Map> toListMap(Iterable iterable, Function keyMapper, Function valueMapper) { + return toListMap(MapUtil.newHashMap(), iterable, keyMapper, valueMapper); + } + + /** + * 将列表转成值为List的HashMap + * + * @param resultMap 结果Map,可自定义结果Map类型 + * @param iterable 值列表 + * @param keyMapper Map的键映射 + * @param valueMapper Map中List的值映射 + * @param 列表值类型 + * @param 键类型 + * @param 值类型 + * @return HashMap + * @since 5.3.6 + */ + public static Map> toListMap(Map> resultMap, Iterable iterable, Function keyMapper, Function valueMapper) { + if (null == resultMap) { + resultMap = MapUtil.newHashMap(); + } + if (ObjectUtil.isNull(iterable)) { + return resultMap; + } + + for (T value : iterable) { + resultMap.computeIfAbsent(keyMapper.apply(value), k -> new ArrayList<>()).add(valueMapper.apply(value)); + } + + return resultMap; + } + + /** + * 将列表转成HashMap + * + * @param iterable 值列表 + * @param keyMapper Map的键映射 + * @param 键类型 + * @param 值类型 + * @return HashMap + * @since 5.3.6 + */ + public static Map toMap(Iterable iterable, Function keyMapper) { + return toMap(iterable, keyMapper, v -> v); + } + + /** + * 将列表转成HashMap + * + * @param iterable 值列表 + * @param keyMapper Map的键映射 + * @param valueMapper Map的值映射 + * @param 列表值类型 + * @param 键类型 + * @param 值类型 + * @return HashMap + * @since 5.3.6 + */ + public static Map toMap(Iterable iterable, Function keyMapper, Function valueMapper) { + return toMap(MapUtil.newHashMap(), iterable, keyMapper, valueMapper); + } + + /** + * 将列表转成Map + * + * @param resultMap 结果Map,通过传入map对象决定结果的Map类型 + * @param iterable 值列表 + * @param keyMapper Map的键映射 + * @param valueMapper Map的值映射 + * @param 列表值类型 + * @param 键类型 + * @param 值类型 + * @return HashMap + * @since 5.3.6 + */ + public static Map toMap(Map resultMap, Iterable iterable, Function keyMapper, Function valueMapper) { + if (null == resultMap) { + resultMap = MapUtil.newHashMap(); + } + if (ObjectUtil.isNull(iterable)) { + return resultMap; + } + + for (T value : iterable) { + resultMap.put(keyMapper.apply(value), valueMapper.apply(value)); + } + + return resultMap; + } + + /** + * Iterator转List
+ * 不判断,直接生成新的List + * + * @param 元素类型 + * @param iter {@link Iterator} + * @return List + * @since 4.0.6 + */ + public static List toList(Iterable iter) { + if (null == iter) { + return null; + } + return toList(iter.iterator()); + } + + /** + * Iterator转List
+ * 不判断,直接生成新的List + * + * @param 元素类型 + * @param iter {@link Iterator} + * @return List + * @since 4.0.6 + */ + public static List toList(Iterator iter) { + return ListUtil.toList(iter); + } + + /** + * Enumeration转换为Iterator + *

+ * Adapt the specified {@code Enumeration} to the {@code Iterator} interface + * + * @param 集合元素类型 + * @param e {@link Enumeration} + * @return {@link Iterator} + */ + public static Iterator asIterator(Enumeration e) { + return new EnumerationIter<>(e); + } + + /** + * {@link Iterator} 转为 {@link Iterable} + * + * @param 元素类型 + * @param iter {@link Iterator} + * @return {@link Iterable} + */ + public static Iterable asIterable(final Iterator iter) { + return () -> iter; + } + + /** + * 遍历{@link Iterator},获取指定index位置的元素 + * + * @param iterator {@link Iterator} + * @param index 位置 + * @param 元素类型 + * @return 元素,找不到元素返回{@code null} + * @since 5.8.0 + */ + public static E get(final Iterator iterator, int index) throws IndexOutOfBoundsException { + if(null == iterator){ + return null; + } + Assert.isTrue(index >= 0, "[index] must be >= 0"); + while (iterator.hasNext()) { + index--; + if (-1 == index) { + return iterator.next(); + } + iterator.next(); + } + return null; + } + + /** + * 获取集合的第一个元素,如果集合为空(null或者空集合),返回{@code null} + * + * @param 集合元素类型 + * @param iterable {@link Iterable} + * @return 第一个元素,为空返回{@code null} + */ + public static T getFirst(Iterable iterable) { + if (iterable instanceof List) { + final List list = (List) iterable; + return CollUtil.isEmpty(list) ? null: list.get(0); + } + + return getFirst(getIter(iterable)); + } + + /** + * 获取集合的第一个非空元素 + * + * @param 集合元素类型 + * @param iterable {@link Iterable} + * @return 第一个元素 + * @since 5.7.2 + */ + public static T getFirstNoneNull(Iterable iterable) { + if (null == iterable) { + return null; + } + return getFirstNoneNull(iterable.iterator()); + } + + /** + * 获取集合的第一个元素 + * + * @param 集合元素类型 + * @param iterator {@link Iterator} + * @return 第一个元素 + */ + public static T getFirst(Iterator iterator) { + return get(iterator, 0); + } + + /** + * 获取集合的第一个非空元素 + * + * @param 集合元素类型 + * @param iterator {@link Iterator} + * @return 第一个非空元素,null表示未找到 + * @since 5.7.2 + */ + public static T getFirstNoneNull(Iterator iterator) { + return firstMatch(iterator, Objects::nonNull); + } + + /** + * 返回{@link Iterator}中第一个匹配规则的值 + * + * @param 数组元素类型 + * @param iterator {@link Iterator} + * @param matcher 匹配接口,实现此接口自定义匹配规则 + * @return 匹配元素,如果不存在匹配元素或{@link Iterator}为空,返回 {@code null} + * @since 5.7.5 + */ + public static T firstMatch(Iterator iterator, Matcher matcher) { + Assert.notNull(matcher, "Matcher must be not null !"); + if (null != iterator) { + while (iterator.hasNext()) { + final T next = iterator.next(); + if (matcher.match(next)) { + return next; + } + } + } + return null; + } + + /** + * 获得{@link Iterable}对象的元素类型(通过第一个非空元素判断)
+ * 注意,此方法至少会调用多次next方法 + * + * @param iterable {@link Iterable} + * @return 元素类型,当列表为空或元素全部为null时,返回null + */ + public static Class getElementType(Iterable iterable) { + return getElementType(getIter(iterable)); + } + + /** + * 获得{@link Iterator}对象的元素类型(通过第一个非空元素判断)
+ * 注意,此方法至少会调用多次next方法 + * + * @param iterator {@link Iterator},为 {@code null}返回{@code null} + * @return 元素类型,当列表为空或元素全部为{@code null}时,返回{@code null} + */ + public static Class getElementType(Iterator iterator) { + if (null == iterator) { + return null; + } + final Object ele = getFirstNoneNull(iterator); + return null == ele ? null : ele.getClass(); + } + + /** + * 编辑,此方法产生一个新{@link ArrayList}
+ * 编辑过程通过传入的Editor实现来返回需要的元素内容,这个Editor实现可以实现以下功能: + * + *

+	 * 1、过滤出需要的对象,如果返回null表示这个元素对象抛弃
+	 * 2、修改元素对象,返回集合中为修改后的对象
+	 * 
+ * + * @param 集合元素类型 + * @param iter 集合 + * @param editor 编辑器接口, {@code null}表示不编辑 + * @return 过滤后的集合 + * @since 5.7.1 + */ + public static List edit(Iterable iter, Editor editor) { + final List result = new ArrayList<>(); + if (null == iter) { + return result; + } + + T modified; + for (T t : iter) { + modified = (null == editor) ? t : editor.edit(t); + if (null != modified) { + result.add(modified); + } + } + return result; + } + + /** + * 过滤集合,此方法在原集合上直接修改
+ * 通过实现Filter接口,完成元素的过滤,这个Filter实现可以实现以下功能: + * + *
+	 * 1、过滤出需要的对象,{@link Filter#accept(Object)}方法返回false的对象将被使用{@link Iterator#remove()}方法移除
+	 * 
+ * + * @param 集合类型 + * @param 集合元素类型 + * @param iter 集合 + * @param filter 过滤器接口 + * @return 编辑后的集合 + * @since 4.6.5 + */ + public static , E> T filter(T iter, Filter filter) { + if (null == iter) { + return null; + } + + filter(iter.iterator(), filter); + + return iter; + } + + /** + * 过滤集合,此方法在原集合上直接修改
+ * 通过实现Filter接口,完成元素的过滤,这个Filter实现可以实现以下功能: + * + *
+	 * 1、过滤出需要的对象,{@link Filter#accept(Object)}方法返回false的对象将被使用{@link Iterator#remove()}方法移除
+	 * 
+ * + * @param 集合元素类型 + * @param iter 集合 + * @param filter 过滤器接口,删除{@link Filter#accept(Object)}为{@code false}的元素 + * @return 编辑后的集合 + * @since 4.6.5 + */ + public static Iterator filter(Iterator iter, Filter filter) { + if (null == iter || null == filter) { + return iter; + } + + while (iter.hasNext()) { + if (!filter.accept(iter.next())) { + iter.remove(); + } + } + return iter; + } + + /** + * 过滤{@link Iterator}并将过滤后满足条件的元素添加到List中 + * + * @param 元素类型 + * @param iter {@link Iterator} + * @param filter 过滤器,保留{@link Filter#accept(Object)}为{@code true}的元素 + * @return ArrayList + * @since 5.7.22 + */ + public static List filterToList(Iterator iter, Filter filter) { + return toList(filtered(iter, filter)); + } + + /** + * 获取一个新的 {@link FilterIter},用于过滤指定元素 + * + * @param iterator 被包装的 {@link Iterator} + * @param filter 过滤断言,当{@link Filter#accept(Object)}为{@code true}时保留元素,{@code false}抛弃元素 + * @param 元素类型 + * @return {@link FilterIter} + * @since 5.8.0 + */ + public static FilterIter filtered(final Iterator iterator, final Filter filter) { + return new FilterIter<>(iterator, filter); + } + + /** + * Iterator转换为Map,转换规则为:
+ * 按照keyFunc函数规则根据元素对象生成Key,元素作为值 + * + * @param Map键类型 + * @param Map值类型 + * @param iterator 数据列表 + * @param map Map对象,转换后的键值对加入此Map,通过传入此对象自定义Map类型 + * @param keyFunc 生成key的函数 + * @return 生成的map + * @since 5.2.6 + */ + public static Map toMap(Iterator iterator, Map map, Func1 keyFunc) { + return toMap(iterator, map, keyFunc, (value) -> value); + } + + /** + * 集合转换为Map,转换规则为:
+ * 按照keyFunc函数规则根据元素对象生成Key,按照valueFunc函数规则根据元素对象生成value组成新的Map + * + * @param Map键类型 + * @param Map值类型 + * @param 元素类型 + * @param iterator 数据列表 + * @param map Map对象,转换后的键值对加入此Map,通过传入此对象自定义Map类型 + * @param keyFunc 生成key的函数 + * @param valueFunc 生成值的策略函数 + * @return 生成的map + * @since 5.2.6 + */ + public static Map toMap(Iterator iterator, Map map, Func1 keyFunc, Func1 valueFunc) { + if (null == iterator) { + return map; + } + + if (null == map) { + map = MapUtil.newHashMap(true); + } + + E element; + while (iterator.hasNext()) { + element = iterator.next(); + try { + map.put(keyFunc.call(element), valueFunc.call(element)); + } catch (Exception e) { + throw new UtilException(e); + } + } + return map; + } + + /** + * 返回一个空Iterator + * + * @param 元素类型 + * @return 空Iterator + * @see Collections#emptyIterator() + * @since 5.3.1 + */ + public static Iterator empty() { + return Collections.emptyIterator(); + } + + /** + * 按照给定函数,转换{@link Iterator}为另一种类型的{@link Iterator} + * + * @param 源元素类型 + * @param 目标元素类型 + * @param iterator 源{@link Iterator} + * @param function 转换函数 + * @return 转换后的{@link Iterator} + * @since 5.4.3 + */ + public static Iterator trans(Iterator iterator, Function function) { + return new TransIter<>(iterator, function); + } + + /** + * 返回 Iterable 对象的元素数量 + * + * @param iterable Iterable对象 + * @return Iterable对象的元素数量 + * @since 5.5.0 + */ + public static int size(Iterable iterable) { + if (null == iterable) { + return 0; + } + + if (iterable instanceof Collection) { + return ((Collection) iterable).size(); + } else { + return size(iterable.iterator()); + } + } + + /** + * 返回 Iterator 对象的元素数量 + * + * @param iterator Iterator对象 + * @return Iterator对象的元素数量 + * @since 5.5.0 + */ + public static int size(Iterator iterator) { + int size = 0; + if (iterator != null) { + while (iterator.hasNext()) { + iterator.next(); + size++; + } + } + return size; + } + + /** + * 判断两个{@link Iterable} 是否元素和顺序相同,返回{@code true}的条件是: + *
    + *
  • 两个{@link Iterable}必须长度相同
  • + *
  • 两个{@link Iterable}元素相同index的对象必须equals,满足{@link Objects#equals(Object, Object)}
  • + *
+ * 此方法来自Apache-Commons-Collections4。 + * + * @param list1 列表1 + * @param list2 列表2 + * @return 是否相同 + * @since 5.6.0 + */ + public static boolean isEqualList(Iterable list1, Iterable list2) { + if (list1 == list2) { + return true; + } + + final Iterator it1 = list1.iterator(); + final Iterator it2 = list2.iterator(); + Object obj1; + Object obj2; + while (it1.hasNext() && it2.hasNext()) { + obj1 = it1.next(); + obj2 = it2.next(); + + if (!Objects.equals(obj1, obj2)) { + return false; + } + } + + // 当两个Iterable长度不一致时返回false + return !(it1.hasNext() || it2.hasNext()); + } + + /** + * 清空指定{@link Iterator},此方法遍历后调用{@link Iterator#remove()}移除每个元素 + * + * @param iterator {@link Iterator} + * @since 5.7.23 + */ + public static void clear(Iterator iterator) { + if (null != iterator) { + while (iterator.hasNext()) { + iterator.next(); + iterator.remove(); + } + } + } + + /** + * 遍历{@link Iterator}
+ * 当consumer为{@code null}表示不处理,但是依旧遍历{@link Iterator} + * + * @param iterator {@link Iterator} + * @param consumer 节点消费,{@code null}表示不处理 + * @param 元素类型 + * @since 5.8.0 + */ + public static void forEach(final Iterator iterator, final Consumer consumer) { + if (iterator != null) { + while (iterator.hasNext()) { + final E element = iterator.next(); + if (null != consumer) { + consumer.accept(element); + } + } + } + } + + /** + * 拼接 {@link Iterator}为字符串 + * + * @param iterator {@link Iterator} + * @param 元素类型 + * @return 字符串 + * @since 5.8.0 + */ + public static String toStr(final Iterator iterator) { + return toStr(iterator, ObjectUtil::toString); + } + + /** + * 拼接 {@link Iterator}为字符串 + * + * @param iterator {@link Iterator} + * @param transFunc 元素转字符串函数 + * @param 元素类型 + * @return 字符串 + * @since 5.8.0 + */ + public static String toStr(final Iterator iterator, final Function transFunc) { + return toStr(iterator, transFunc, ", ", "[", "]"); + } + + /** + * 拼接 {@link Iterator}为字符串 + * + * @param iterator {@link Iterator} + * @param transFunc 元素转字符串函数 + * @param delimiter 分隔符 + * @param prefix 前缀 + * @param suffix 后缀 + * @param 元素类型 + * @return 字符串 + * @since 5.8.0 + */ + public static String toStr(final Iterator iterator, + final Function transFunc, + final String delimiter, + final String prefix, + final String suffix) { + final StrJoiner strJoiner = StrJoiner.of(delimiter, prefix, suffix); + strJoiner.append(iterator, transFunc); + return strJoiner.toString(); + } + + /** + * 从给定的对象中获取可能存在的{@link Iterator},规则如下: + *
    + *
  • null - null
  • + *
  • Iterator - 直接返回
  • + *
  • Enumeration - {@link EnumerationIter}
  • + *
  • Collection - 调用{@link Collection#iterator()}
  • + *
  • Map - Entry的{@link Iterator}
  • + *
  • Dictionary - values (elements) enumeration returned as iterator
  • + *
  • array - {@link ArrayIter}
  • + *
  • NodeList - {@link NodeListIter}
  • + *
  • Node - 子节点
  • + *
  • object with iterator() public method,通过反射访问
  • + *
  • object - 单对象的{@link ArrayIter}
  • + *
+ * + * @param obj 可以获取{@link Iterator}的对象 + * @return {@link Iterator},如果提供对象为{@code null},返回{@code null} + */ + public static Iterator getIter(final Object obj) { + if (obj == null) { + return null; + } else if (obj instanceof Iterator) { + return (Iterator) obj; + } else if (obj instanceof Iterable) { + return ((Iterable) obj).iterator(); + } else if (ArrayUtil.isArray(obj)) { + return new ArrayIter<>(obj); + } else if (obj instanceof Enumeration) { + return new EnumerationIter<>((Enumeration) obj); + } else if (obj instanceof Map) { + return ((Map) obj).entrySet().iterator(); + } else if (obj instanceof NodeList) { + return new NodeListIter((NodeList) obj); + } else if (obj instanceof Node) { + // 遍历子节点 + return new NodeListIter(((Node) obj).getChildNodes()); + } else if (obj instanceof Dictionary) { + return new EnumerationIter<>(((Dictionary) obj).elements()); + } + + // 反射获取 + try { + final Object iterator = ReflectUtil.invoke(obj, "iterator"); + if (iterator instanceof Iterator) { + return (Iterator) iterator; + } + } catch (final RuntimeException ignore) { + // ignore + } + return new ArrayIter<>(new Object[]{obj}); + } +} diff --git a/src/main/java/cn/hutool/core/collection/IterableIter.java b/src/main/java/cn/hutool/core/collection/IterableIter.java new file mode 100644 index 0000000..e5e91e5 --- /dev/null +++ b/src/main/java/cn/hutool/core/collection/IterableIter.java @@ -0,0 +1,18 @@ +package cn.hutool.core.collection; + +import java.util.Iterator; + +/** + * 提供合成接口,共同提供{@link Iterable}和{@link Iterator}功能 + * + * @param 节点类型 + * @author looly + * @since 5.7.14 + */ +public interface IterableIter extends Iterable, Iterator { + + @Override + default Iterator iterator() { + return this; + } +} diff --git a/src/main/java/cn/hutool/core/collection/IteratorEnumeration.java b/src/main/java/cn/hutool/core/collection/IteratorEnumeration.java new file mode 100644 index 0000000..9beea2c --- /dev/null +++ b/src/main/java/cn/hutool/core/collection/IteratorEnumeration.java @@ -0,0 +1,37 @@ +package cn.hutool.core.collection; + +import java.io.Serializable; +import java.util.Enumeration; +import java.util.Iterator; + +/** + * {@link Iterator}对象转{@link Enumeration} + * @author Looly + * + * @param 元素类型 + * @since 3.0.8 + */ +public class IteratorEnumeration implements Enumeration, Serializable{ + private static final long serialVersionUID = 1L; + + private final Iterator iterator; + + /** + * 构造 + * @param iterator {@link Iterator}对象 + */ + public IteratorEnumeration(Iterator iterator) { + this.iterator = iterator; + } + + @Override + public boolean hasMoreElements() { + return iterator.hasNext(); + } + + @Override + public E nextElement() { + return iterator.next(); + } + +} diff --git a/src/main/java/cn/hutool/core/collection/LineIter.java b/src/main/java/cn/hutool/core/collection/LineIter.java new file mode 100644 index 0000000..e6bf99c --- /dev/null +++ b/src/main/java/cn/hutool/core/collection/LineIter.java @@ -0,0 +1,101 @@ +package cn.hutool.core.collection; + +import cn.hutool.core.io.IORuntimeException; +import cn.hutool.core.io.IoUtil; +import cn.hutool.core.lang.Assert; + +import java.io.BufferedReader; +import java.io.Closeable; +import java.io.IOException; +import java.io.InputStream; +import java.io.Reader; +import java.io.Serializable; +import java.nio.charset.Charset; + +/** + * 将Reader包装为一个按照行读取的Iterator
+ * 此对象遍历结束后,应关闭之,推荐使用方式: + * + *
+ * LineIterator it = null;
+ * try {
+ * 	it = new LineIterator(reader);
+ * 	while (it.hasNext()) {
+ * 		String line = it.nextLine();
+ * 		// do something with line
+ * 	}
+ * } finally {
+ * 		it.close();
+ * }
+ * 
+ * + * 此类来自于Apache Commons io + * + * @author looly + * @since 4.1.1 + */ +public class LineIter extends ComputeIter implements IterableIter, Closeable, Serializable { + private static final long serialVersionUID = 1L; + + private final BufferedReader bufferedReader; + + /** + * 构造 + * + * @param in {@link InputStream} + * @param charset 编码 + * @throws IllegalArgumentException reader为null抛出此异常 + */ + public LineIter(InputStream in, Charset charset) throws IllegalArgumentException { + this(IoUtil.getReader(in, charset)); + } + + /** + * 构造 + * + * @param reader {@link Reader}对象,不能为null + * @throws IllegalArgumentException reader为null抛出此异常 + */ + public LineIter(Reader reader) throws IllegalArgumentException { + Assert.notNull(reader, "Reader must not be null"); + this.bufferedReader = IoUtil.getReader(reader); + } + + // ----------------------------------------------------------------------- + @Override + protected String computeNext() { + try { + while (true) { + String line = bufferedReader.readLine(); + if (line == null) { + return null; + } else if (isValidLine(line)) { + return line; + } + // 无效行,则跳过进入下一行 + } + } catch (IOException ioe) { + close(); + throw new IORuntimeException(ioe); + } + } + + /** + * 关闭Reader + */ + @Override + public void close() { + super.finish(); + IoUtil.close(bufferedReader); + } + + /** + * 重写此方法来判断是否每一行都被返回,默认全部为true + * + * @param line 需要验证的行 + * @return 是否通过验证 + */ + protected boolean isValidLine(String line) { + return true; + } +} diff --git a/src/main/java/cn/hutool/core/collection/ListUtil.java b/src/main/java/cn/hutool/core/collection/ListUtil.java new file mode 100644 index 0000000..aa0e8af --- /dev/null +++ b/src/main/java/cn/hutool/core/collection/ListUtil.java @@ -0,0 +1,677 @@ +package cn.hutool.core.collection; + +import cn.hutool.core.comparator.PinyinComparator; +import cn.hutool.core.comparator.PropertyComparator; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.lang.Matcher; +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.PageUtil; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.Enumeration; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.RandomAccess; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.function.Consumer; + +/** + * List相关工具类 + * + * @author looly + */ +public class ListUtil { + /** + * 新建一个空List + * + * @param 集合元素类型 + * @param isLinked 是否新建LinkedList + * @return List对象 + * @since 4.1.2 + */ + public static List list(boolean isLinked) { + return isLinked ? new LinkedList<>() : new ArrayList<>(); + } + + /** + * 新建一个List + * + * @param 集合元素类型 + * @param isLinked 是否新建LinkedList + * @param values 数组 + * @return List对象 + * @since 4.1.2 + */ + @SafeVarargs + public static List list(boolean isLinked, T... values) { + if (ArrayUtil.isEmpty(values)) { + return list(isLinked); + } + final List arrayList = isLinked ? new LinkedList<>() : new ArrayList<>(values.length); + Collections.addAll(arrayList, values); + return arrayList; + } + + /** + * 新建一个List + * + * @param 集合元素类型 + * @param isLinked 是否新建LinkedList + * @param collection 集合 + * @return List对象 + * @since 4.1.2 + */ + public static List list(boolean isLinked, Collection collection) { + if (null == collection) { + return list(isLinked); + } + return isLinked ? new LinkedList<>(collection) : new ArrayList<>(collection); + } + + /** + * 新建一个List
+ * 提供的参数为null时返回空{@link ArrayList} + * + * @param 集合元素类型 + * @param isLinked 是否新建LinkedList + * @param iterable {@link Iterable} + * @return List对象 + * @since 4.1.2 + */ + public static List list(boolean isLinked, Iterable iterable) { + if (null == iterable) { + return list(isLinked); + } + return list(isLinked, iterable.iterator()); + } + + /** + * 新建一个List
+ * 提供的参数为null时返回空{@link ArrayList} + * + * @param 集合元素类型 + * @param isLinked 是否新建LinkedList + * @param iter {@link Iterator} + * @return ArrayList对象 + * @since 4.1.2 + */ + public static List list(boolean isLinked, Iterator iter) { + final List list = list(isLinked); + if (null != iter) { + while (iter.hasNext()) { + list.add(iter.next()); + } + } + return list; + } + + /** + * 新建一个List
+ * 提供的参数为null时返回空{@link ArrayList} + * + * @param 集合元素类型 + * @param isLinked 是否新建LinkedList + * @param enumration {@link Enumeration} + * @return ArrayList对象 + * @since 3.0.8 + */ + public static List list(boolean isLinked, Enumeration enumration) { + final List list = list(isLinked); + if (null != enumration) { + while (enumration.hasMoreElements()) { + list.add(enumration.nextElement()); + } + } + return list; + } + + /** + * 新建一个ArrayList + * + * @param 集合元素类型 + * @param values 数组 + * @return ArrayList对象 + */ + @SafeVarargs + public static ArrayList toList(T... values) { + return (ArrayList) list(false, values); + } + + /** + * 新建LinkedList + * + * @param values 数组 + * @param 类型 + * @return LinkedList + * @since 4.1.2 + */ + @SafeVarargs + public static LinkedList toLinkedList(T... values) { + return (LinkedList) list(true, values); + } + + /** + * 数组转为一个不可变List
+ * 类似于Java9中的List.of + * + * @param ts 对象 + * @param 对象类型 + * @return 不可修改List + * @since 5.4.3 + */ + @SafeVarargs + public static List of(T... ts) { + if (ArrayUtil.isEmpty(ts)) { + return Collections.emptyList(); + } + return Collections.unmodifiableList(toList(ts)); + } + + /** + * 新建一个CopyOnWriteArrayList + * + * @param 集合元素类型 + * @param collection 集合 + * @return {@link CopyOnWriteArrayList} + */ + public static CopyOnWriteArrayList toCopyOnWriteArrayList(Collection collection) { + return (null == collection) ? (new CopyOnWriteArrayList<>()) : (new CopyOnWriteArrayList<>(collection)); + } + + /** + * 新建一个ArrayList + * + * @param 集合元素类型 + * @param collection 集合 + * @return ArrayList对象 + */ + public static ArrayList toList(Collection collection) { + return (ArrayList) list(false, collection); + } + + /** + * 新建一个ArrayList
+ * 提供的参数为null时返回空{@link ArrayList} + * + * @param 集合元素类型 + * @param iterable {@link Iterable} + * @return ArrayList对象 + * @since 3.1.0 + */ + public static ArrayList toList(Iterable iterable) { + return (ArrayList) list(false, iterable); + } + + /** + * 新建一个ArrayList
+ * 提供的参数为null时返回空{@link ArrayList} + * + * @param 集合元素类型 + * @param iterator {@link Iterator} + * @return ArrayList对象 + * @since 3.0.8 + */ + public static ArrayList toList(Iterator iterator) { + return (ArrayList) list(false, iterator); + } + + /** + * 新建一个ArrayList
+ * 提供的参数为null时返回空{@link ArrayList} + * + * @param 集合元素类型 + * @param enumeration {@link Enumeration} + * @return ArrayList对象 + * @since 3.0.8 + */ + public static ArrayList toList(Enumeration enumeration) { + return (ArrayList) list(false, enumeration); + } + + /** + * 对指定List分页取值 + * + * @param 集合元素类型 + * @param pageNo 页码,第一页的页码取决于{@link PageUtil#getFirstPageNo()},默认0 + * @param pageSize 每页的条目数 + * @param list 列表 + * @return 分页后的段落内容 + * @since 4.1.20 + */ + public static List page(int pageNo, int pageSize, List list) { + if (CollUtil.isEmpty(list)) { + return new ArrayList<>(0); + } + + int resultSize = list.size(); + // 每页条目数大于总数直接返回所有 + if (resultSize <= pageSize) { + if (pageNo < (PageUtil.getFirstPageNo() + 1)) { + return unmodifiable(list); + } else { + // 越界直接返回空 + return new ArrayList<>(0); + } + } + // 相乘可能会导致越界 临时用long + if (((long) (pageNo - PageUtil.getFirstPageNo()) * pageSize) > resultSize) { + // 越界直接返回空 + return new ArrayList<>(0); + } + + final int[] startEnd = PageUtil.transToStartEnd(pageNo, pageSize); + if (startEnd[1] > resultSize) { + startEnd[1] = resultSize; + if (startEnd[0] > startEnd[1]) { + return new ArrayList<>(0); + } + } + + return sub(list, startEnd[0], startEnd[1]); + } + + /** + * 对指定List进行分页,逐页返回数据 + * + * @param 集合元素类型 + * @param list 源数据列表 + * @param pageSize 每页的条目数 + * @param pageListConsumer 单页数据函数式返回 + * @since 5.7.10 + */ + public static void page(List list, int pageSize, Consumer> pageListConsumer) { + if (CollUtil.isEmpty(list) || pageSize <= 0) { + return; + } + + final int total = list.size(); + final int totalPage = PageUtil.totalPage(total, pageSize); + for (int pageNo = PageUtil.getFirstPageNo(); pageNo < totalPage + PageUtil.getFirstPageNo(); pageNo++) { + // 获取当前页在列表中对应的起止序号 + final int[] startEnd = PageUtil.transToStartEnd(pageNo, pageSize); + if (startEnd[1] > total) { + startEnd[1] = total; + } + + // 返回数据 + pageListConsumer.accept(sub(list, startEnd[0], startEnd[1])); + } + } + + /** + * 针对List排序,排序会修改原List + * + * @param 元素类型 + * @param list 被排序的List + * @param c {@link Comparator} + * @return 原list + * @see Collections#sort(List, Comparator) + */ + public static List sort(List list, Comparator c) { + if (CollUtil.isEmpty(list)) { + return list; + } + list.sort(c); + return list; + } + + /** + * 根据Bean的属性排序 + * + * @param 元素类型 + * @param list List + * @param property 属性名 + * @return 排序后的List + * @since 4.0.6 + */ + public static List sortByProperty(List list, String property) { + return sort(list, new PropertyComparator<>(property)); + } + + /** + * 根据汉字的拼音顺序排序 + * + * @param list List + * @return 排序后的List + * @since 4.0.8 + */ + public static List sortByPinyin(List list) { + return sort(list, new PinyinComparator()); + } + + /** + * 反序给定List,会在原List基础上直接修改 + * + * @param 元素类型 + * @param list 被反转的List + * @return 反转后的List + * @since 4.0.6 + */ + public static List reverse(List list) { + Collections.reverse(list); + return list; + } + + /** + * 反序给定List,会创建一个新的List,原List数据不变 + * + * @param 元素类型 + * @param list 被反转的List + * @return 反转后的List + * @since 4.0.6 + */ + public static List reverseNew(List list) { + List list2 = ObjectUtil.clone(list); + if (null == list2) { + // 不支持clone + list2 = new ArrayList<>(list); + } + return reverse(list2); + } + + /** + * 设置或增加元素。当index小于List的长度时,替换指定位置的值,否则在尾部追加 + * + * @param 元素类型 + * @param list List列表 + * @param index 位置 + * @param element 新元素 + * @return 原List + * @since 4.1.2 + */ + public static List setOrAppend(List list, int index, T element) { + Assert.notNull(list, "List must be not null !"); + if (index < list.size()) { + list.set(index, element); + } else { + list.add(element); + } + return list; + } + + /** + * 在指定位置设置元素。当index小于List的长度时,替换指定位置的值,否则追加{@code null}直到到达index后,设置值 + * + * @param 元素类型 + * @param list List列表 + * @param index 位置 + * @param element 新元素 + * @return 原List + * @since 5。8.4 + */ + public static List setOrPadding(List list, int index, T element) { + return setOrPadding(list, index, element, null); + } + + /** + * 在指定位置设置元素。当index小于List的长度时,替换指定位置的值,否则追加{@code paddingElement}直到到达index后,设置值 + * + * @param 元素类型 + * @param list List列表 + * @param index 位置 + * @param element 新元素 + * @param paddingElement 填充的值 + * @return 原List + * @since 5。8.4 + */ + public static List setOrPadding(List list, int index, T element, T paddingElement) { + Assert.notNull(list, "List must be not null !"); + final int size = list.size(); + if (index < size) { + list.set(index, element); + } else { + for (int i = size; i < index; i++) { + list.add(paddingElement); + } + list.add(element); + } + return list; + } + + /** + * 截取集合的部分 + * + * @param 集合元素类型 + * @param list 被截取的数组 + * @param start 开始位置(包含) + * @param end 结束位置(不包含) + * @return 截取后的数组,当开始位置超过最大时,返回空的List + */ + public static List + sub(List list, int start, int end) { + return sub(list, start, end, 1); + } + + /** + * 截取集合的部分
+ * 此方法与{@link List#subList(int, int)} 不同在于子列表是新的副本,操作子列表不会影响原列表。 + * + * @param 集合元素类型 + * @param list 被截取的数组 + * @param start 开始位置(包含) + * @param end 结束位置(不包含) + * @param step 步进 + * @return 截取后的数组,当开始位置超过最大时,返回空的List + * @since 4.0.6 + */ + public static List sub(List list, int start, int end, int step) { + if (list == null) { + return null; + } + + if (list.isEmpty()) { + return new ArrayList<>(0); + } + + final int size = list.size(); + if (start < 0) { + start += size; + } + if (end < 0) { + end += size; + } + if (start == size) { + return new ArrayList<>(0); + } + if (start > end) { + int tmp = start; + start = end; + end = tmp; + } + if (end > size) { + if (start >= size) { + return new ArrayList<>(0); + } + end = size; + } + + if (step < 1) { + step = 1; + } + + final List result = new ArrayList<>(); + for (int i = start; i < end; i += step) { + result.add(list.get(i)); + } + return result; + } + + /** + * 获取匹配规则定义中匹配到元素的最后位置
+ * 此方法对于某些无序集合的位置信息,以转换为数组后的位置为准。 + * + * @param 元素类型 + * @param list List集合 + * @param matcher 匹配器,为空则全部匹配 + * @return 最后一个位置 + * @since 5.6.6 + */ + public static int lastIndexOf(List list, Matcher matcher) { + if (null != list) { + final int size = list.size(); + if (size > 0) { + for (int i = size - 1; i >= 0; i--) { + if (null == matcher || matcher.match(list.get(i))) { + return i; + } + } + } + } + return -1; + } + + /** + * 获取匹配规则定义中匹配到元素的所有位置 + * + * @param 元素类型 + * @param list 列表 + * @param matcher 匹配器,为空则全部匹配 + * @return 位置数组 + * @since 5.2.5 + */ + public static int[] indexOfAll(List list, Matcher matcher) { + return CollUtil.indexOfAll(list, matcher); + } + + /** + * 将对应List转换为不可修改的List + * + * @param list List + * @param 元素类型 + * @return 不可修改List + * @since 5.2.6 + */ + public static List unmodifiable(List list) { + if (null == list) { + return null; + } + return Collections.unmodifiableList(list); + } + + /** + * 获取一个空List,这个空List不可变 + * + * @param 元素类型 + * @return 空的List + * @see Collections#emptyList() + * @since 5.2.6 + */ + public static List empty() { + return Collections.emptyList(); + } + + /** + * 通过传入分区长度,将指定列表分区为不同的块,每块区域的长度相同(最后一块可能小于长度)
+ * 分区是在原List的基础上进行的,返回的分区是不可变的抽象列表,原列表元素变更,分区中元素也会变更。 + * + *

+ * 需要特别注意的是,此方法调用{@link List#subList(int, int)}切分List, + * 此方法返回的是原List的视图,也就是说原List有变更,切分后的结果也会变更。 + *

+ * + * @param 集合元素类型 + * @param list 列表,为空时返回{@link #empty()} + * @param size 每个段的长度,当长度超过list长度时,size按照list长度计算,即只返回一个节点 + * @return 分段列表 + * @since 5.4.5 + */ + public static List> partition(List list, int size) { + if (CollUtil.isEmpty(list)) { + return empty(); + } + + return (list instanceof RandomAccess) + ? new RandomAccessPartition<>(list, size) + : new Partition<>(list, size); + } + + /** + * 对集合按照指定长度分段,每一个段为单独的集合,返回这个集合的列表 + * + *

+ * 需要特别注意的是,此方法调用{@link List#subList(int, int)}切分List, + * 此方法返回的是原List的视图,也就是说原List有变更,切分后的结果也会变更。 + *

+ * + * @param 集合元素类型 + * @param list 列表,为空时返回{@link #empty()} + * @param size 每个段的长度,当长度超过list长度时,size按照list长度计算,即只返回一个节点 + * @return 分段列表 + * @see #partition(List, int) + * @since 5.4.5 + */ + public static List> split(List list, int size) { + return partition(list, size); + } + + /** + * 将集合平均分成多个list,返回这个集合的列表 + *

例:

+ *
+	 *     ListUtil.splitAvg(null, 3);	// []
+	 *     ListUtil.splitAvg(Arrays.asList(1, 2, 3, 4), 2);	// [[1, 2], [3, 4]]
+	 *     ListUtil.splitAvg(Arrays.asList(1, 2, 3), 5);	// [[1], [2], [3], [], []]
+	 *     ListUtil.splitAvg(Arrays.asList(1, 2, 3), 2);	// [[1, 2], [3]]
+	 * 
+ * + * @param 集合元素类型 + * @param list 集合 + * @param limit 要均分成几个list + * @return 分段列表 + * @author lileming + * @since 5.7.10 + */ + public static List> splitAvg(List list, int limit) { + if (CollUtil.isEmpty(list)) { + return empty(); + } + + return (list instanceof RandomAccess) + ? new RandomAccessAvgPartition<>(list, limit) + : new AvgPartition<>(list, limit); + } + + /** + * 将指定元素交换到指定索引位置,其他元素的索引值不变
+ * 交换会修改原List
+ * 如果集合中有多个相同元素,只交换第一个找到的元素 + * + * @param 元素类型 + * @param list 列表 + * @param element 需交换元素 + * @param targetIndex 目标索引 + * @since 5.7.13 + */ + public static void swapTo(List list, T element, Integer targetIndex) { + if (CollUtil.isNotEmpty(list)) { + final int index = list.indexOf(element); + if (index >= 0) { + Collections.swap(list, index, targetIndex); + } + } + } + + /** + * 将指定元素交换到指定元素位置,其他元素的索引值不变
+ * 交换会修改原List
+ * 如果集合中有多个相同元素,只交换第一个找到的元素 + * + * @param 元素类型 + * @param list 列表 + * @param element 需交换元素 + * @param targetElement 目标元素 + */ + public static void swapElement(List list, T element, T targetElement) { + if (CollUtil.isNotEmpty(list)) { + final int targetIndex = list.indexOf(targetElement); + if (targetIndex >= 0) { + swapTo(list, element, targetIndex); + } + } + } +} diff --git a/src/main/java/cn/hutool/core/collection/NodeListIter.java b/src/main/java/cn/hutool/core/collection/NodeListIter.java new file mode 100644 index 0000000..e129255 --- /dev/null +++ b/src/main/java/cn/hutool/core/collection/NodeListIter.java @@ -0,0 +1,63 @@ +package cn.hutool.core.collection; + +import cn.hutool.core.lang.Assert; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; + +import java.util.Iterator; +import java.util.NoSuchElementException; + +/** + * 包装 {@link NodeList} 的{@link Iterator} + *

+ * 此 iterator 不支持 {@link #remove()} 方法。 + * + * @author apache commons,looly + * @see NodeList + * @since 5.8.0 + */ +public class NodeListIter implements ResettableIter { + + private final NodeList nodeList; + /** + * 当前位置索引 + */ + private int index = 0; + + /** + * 构造, 根据给定{@link NodeList} 创建{@code NodeListIterator} + * + * @param nodeList {@link NodeList},非空 + */ + public NodeListIter(final NodeList nodeList) { + this.nodeList = Assert.notNull(nodeList, "NodeList must not be null."); + } + + @Override + public boolean hasNext() { + return nodeList != null && index < nodeList.getLength(); + } + + @Override + public Node next() { + if (nodeList != null && index < nodeList.getLength()) { + return nodeList.item(index++); + } + throw new NoSuchElementException("underlying nodeList has no more elements"); + } + + /** + * Throws {@link UnsupportedOperationException}. + * + * @throws UnsupportedOperationException always + */ + @Override + public void remove() { + throw new UnsupportedOperationException("remove() method not supported for a NodeListIterator."); + } + + @Override + public void reset() { + this.index = 0; + } +} diff --git a/src/main/java/cn/hutool/core/collection/Partition.java b/src/main/java/cn/hutool/core/collection/Partition.java new file mode 100644 index 0000000..8b10758 --- /dev/null +++ b/src/main/java/cn/hutool/core/collection/Partition.java @@ -0,0 +1,59 @@ +package cn.hutool.core.collection; + +import cn.hutool.core.lang.Assert; + +import java.util.AbstractList; +import java.util.List; + +/** + * 列表分区或分段
+ * 通过传入分区长度,将指定列表分区为不同的块,每块区域的长度相同(最后一块可能小于长度)
+ * 分区是在原List的基础上进行的,返回的分区是不可变的抽象列表,原列表元素变更,分区中元素也会变更。 + * 参考:Guava的Lists#Partition + * + * @param 元素类型 + * @author looly, guava + * @since 5.7.10 + */ +public class Partition extends AbstractList> { + + protected final List list; + protected final int size; + + /** + * 列表分区 + * + * @param list 被分区的列表,非空 + * @param size 每个分区的长度,必须>0 + */ + public Partition(List list, int size) { + this.list = Assert.notNull(list); + this.size = Math.min(list.size(), size); + } + + @Override + public List get(int index) { + final int start = index * size; + final int end = Math.min(start + size, list.size()); + return list.subList(start, end); + } + + @Override + public int size() { + // 此处采用动态计算,以应对list变 + final int size = this.size; + if(0 == size){ + return 0; + } + + final int total = list.size(); + // 类似于判断余数,当总数非整份size时,多余的数>=1,则相当于被除数多一个size,做到+1目的 + // 类似于:if(total % size > 0){length += 1;} + return (total + size - 1) / size; + } + + @Override + public boolean isEmpty() { + return list.isEmpty(); + } +} diff --git a/src/main/java/cn/hutool/core/collection/PartitionIter.java b/src/main/java/cn/hutool/core/collection/PartitionIter.java new file mode 100644 index 0000000..8892e8e --- /dev/null +++ b/src/main/java/cn/hutool/core/collection/PartitionIter.java @@ -0,0 +1,59 @@ +package cn.hutool.core.collection; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +/** + * 分批迭代工具,可以分批处理数据 + *

    + *
  1. 比如调用其他客户的接口,传入的入参有限,需要分批
  2. + *
  3. 比如mysql/oracle用in语句查询,超过1000可以分批
  4. + *
  5. 比如数据库取出游标,可以把游标里的数据一批一批处理
  6. + *
+ * + * @param 字段类型 + * @author qiqi.chen + * @since 5.7.10 + */ +public class PartitionIter implements IterableIter>, Serializable { + private static final long serialVersionUID = 1L; + + /** + * 被分批的迭代器 + */ + protected final Iterator iterator; + /** + * 实际每批大小 + */ + protected final int partitionSize; + + /** + * 创建分组对象 + * + * @param iterator 迭代器 + * @param partitionSize 每批大小,最后一批不满一批算一批 + */ + public PartitionIter(Iterator iterator, int partitionSize) { + this.iterator = iterator; + this.partitionSize = partitionSize; + } + + @Override + public boolean hasNext() { + return this.iterator.hasNext(); + } + + @Override + public List next() { + final List list = new ArrayList<>(this.partitionSize); + for (int i = 0; i < this.partitionSize; i++) { + if (!iterator.hasNext()) { + break; + } + list.add(iterator.next()); + } + return list; + } +} diff --git a/src/main/java/cn/hutool/core/collection/RandomAccessAvgPartition.java b/src/main/java/cn/hutool/core/collection/RandomAccessAvgPartition.java new file mode 100644 index 0000000..fce4c8b --- /dev/null +++ b/src/main/java/cn/hutool/core/collection/RandomAccessAvgPartition.java @@ -0,0 +1,32 @@ +package cn.hutool.core.collection; + +import java.util.List; +import java.util.RandomAccess; + +/** + * 列表分区或分段(可随机访问列表)
+ * 通过传入分区个数,将指定列表分区为不同的块,每块区域的长度均匀分布(个数差不超过1)
+ *
+ *     [1,2,3,4] -》 [1,2], [3, 4]
+ *     [1,2,3,4] -》 [1,2], [3], [4]
+ *     [1,2,3,4] -》 [1], [2], [3], [4]
+ *     [1,2,3,4] -》 [1], [2], [3], [4], []
+ * 
+ * 分区是在原List的基础上进行的,返回的分区是不可变的抽象列表,原列表元素变更,分区中元素也会变更。 + * + * @param 元素类型 + * @author looly + * @since 5.7.10 + */ +public class RandomAccessAvgPartition extends AvgPartition implements RandomAccess { + + /** + * 列表分区 + * + * @param list 被分区的列表 + * @param limit 分区个数 + */ + public RandomAccessAvgPartition(List list, int limit) { + super(list, limit); + } +} diff --git a/src/main/java/cn/hutool/core/collection/RandomAccessPartition.java b/src/main/java/cn/hutool/core/collection/RandomAccessPartition.java new file mode 100644 index 0000000..d6cb3aa --- /dev/null +++ b/src/main/java/cn/hutool/core/collection/RandomAccessPartition.java @@ -0,0 +1,27 @@ +package cn.hutool.core.collection; + +import java.util.List; +import java.util.RandomAccess; + +/** + * 列表分区或分段(可随机访问列表)
+ * 通过传入分区长度,将指定列表分区为不同的块,每块区域的长度相同(最后一块可能小于长度)
+ * 分区是在原List的基础上进行的,返回的分区是不可变的抽象列表,原列表元素变更,分区中元素也会变更。 + * 参考:Guava的Lists#RandomAccessPartition + * + * @param 元素类型 + * @author looly, guava + * @since 5.7.10 + */ +public class RandomAccessPartition extends Partition implements RandomAccess { + + /** + * 构造 + * + * @param list 被分区的列表,必须实现{@link RandomAccess} + * @param size 每个分区的长度 + */ + public RandomAccessPartition(List list, int size) { + super(list, size); + } +} diff --git a/src/main/java/cn/hutool/core/collection/ResettableIter.java b/src/main/java/cn/hutool/core/collection/ResettableIter.java new file mode 100644 index 0000000..837c7dc --- /dev/null +++ b/src/main/java/cn/hutool/core/collection/ResettableIter.java @@ -0,0 +1,18 @@ +package cn.hutool.core.collection; + +import java.util.Iterator; + +/** + * 支持重置的{@link Iterator} 接口
+ * 通过实现{@link #reset()},重置此{@link Iterator}后可实现复用重新遍历 + * + * @param 元素类型 + * @since 5.8.0 + */ +public interface ResettableIter extends Iterator { + + /** + * 重置,重置后可重新遍历 + */ + void reset(); +} diff --git a/src/main/java/cn/hutool/core/collection/RingIndexUtil.java b/src/main/java/cn/hutool/core/collection/RingIndexUtil.java new file mode 100644 index 0000000..34fd8c7 --- /dev/null +++ b/src/main/java/cn/hutool/core/collection/RingIndexUtil.java @@ -0,0 +1,80 @@ +package cn.hutool.core.collection; + +import cn.hutool.core.lang.Assert; + +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; + +/** + * 集合索引环形获取工具类 + * + * @author ZhouChuGang + * @since 5.7.15 + */ +public class RingIndexUtil { + + /** + * 通过cas操作 实现对指定值内的回环累加 + * + * @param object 集合 + *
    + *
  • Collection - the collection size + *
  • Map - the map size + *
  • Array - the array size + *
  • Iterator - the number of elements remaining in the iterator + *
  • Enumeration - the number of elements remaining in the enumeration + *
+ * @param atomicInteger 原子操作类 + * @return 索引位置 + */ + public static int ringNextIntByObj(Object object, AtomicInteger atomicInteger) { + Assert.notNull(object); + int modulo = CollUtil.size(object); + return ringNextInt(modulo, atomicInteger); + } + + /** + * 通过cas操作 实现对指定值内的回环累加 + * + * @param modulo 回环周期值 + * @param atomicInteger 原子操作类 + * @return 索引位置 + */ + public static int ringNextInt(int modulo, AtomicInteger atomicInteger) { + Assert.notNull(atomicInteger); + Assert.isTrue(modulo > 0); + if (modulo <= 1) { + return 0; + } + for (; ; ) { + int current = atomicInteger.get(); + int next = (current + 1) % modulo; + if (atomicInteger.compareAndSet(current, next)) { + return next; + } + } + } + + /** + * 通过cas操作 实现对指定值内的回环累加
+ * 此方法一般用于大量数据完成回环累加(如数据库中的值大于int最大值) + * + * @param modulo 回环周期值 + * @param atomicLong 原子操作类 + * @return 索引位置 + */ + public static long ringNextLong(long modulo, AtomicLong atomicLong) { + Assert.notNull(atomicLong); + Assert.isTrue(modulo > 0); + if (modulo <= 1) { + return 0; + } + for (; ; ) { + long current = atomicLong.get(); + long next = (current + 1) % modulo; + if (atomicLong.compareAndSet(current, next)) { + return next; + } + } + } +} diff --git a/src/main/java/cn/hutool/core/collection/SpliteratorUtil.java b/src/main/java/cn/hutool/core/collection/SpliteratorUtil.java new file mode 100644 index 0000000..630954f --- /dev/null +++ b/src/main/java/cn/hutool/core/collection/SpliteratorUtil.java @@ -0,0 +1,26 @@ +package cn.hutool.core.collection; + +import java.util.Spliterator; +import java.util.function.Function; + +/** + * {@link Spliterator}相关工具类 + * + * @author looly + * @since 5.4.3 + */ +public class SpliteratorUtil { + + /** + * 使用给定的转换函数,转换源{@link Spliterator}为新类型的{@link Spliterator} + * + * @param 源元素类型 + * @param 目标元素类型 + * @param fromSpliterator 源{@link Spliterator} + * @param function 转换函数 + * @return 新类型的{@link Spliterator} + */ + public static Spliterator trans(Spliterator fromSpliterator, Function function) { + return new TransSpliterator<>(fromSpliterator, function); + } +} diff --git a/src/main/java/cn/hutool/core/collection/TransCollection.java b/src/main/java/cn/hutool/core/collection/TransCollection.java new file mode 100644 index 0000000..500d9d3 --- /dev/null +++ b/src/main/java/cn/hutool/core/collection/TransCollection.java @@ -0,0 +1,73 @@ +package cn.hutool.core.collection; + +import cn.hutool.core.lang.Assert; + +import java.util.AbstractCollection; +import java.util.Collection; +import java.util.Iterator; +import java.util.Spliterator; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Predicate; + +/** + * 使用给定的转换函数,转换源集合为新类型的集合 + * + * @param 源元素类型 + * @param 目标元素类型 + * @author looly + * @since 5.4.3 + */ +public class TransCollection extends AbstractCollection { + + private final Collection fromCollection; + private final Function function; + + /** + * 构造 + * + * @param fromCollection 源集合 + * @param function 转换函数 + */ + public TransCollection(Collection fromCollection, Function function) { + this.fromCollection = Assert.notNull(fromCollection); + this.function = Assert.notNull(function); + } + + @Override + public Iterator iterator() { + return IterUtil.trans(fromCollection.iterator(), function); + } + + @Override + public void clear() { + fromCollection.clear(); + } + + @Override + public boolean isEmpty() { + return fromCollection.isEmpty(); + } + + @Override + public void forEach(Consumer action) { + Assert.notNull(action); + fromCollection.forEach((f) -> action.accept(function.apply(f))); + } + + @Override + public boolean removeIf(Predicate filter) { + Assert.notNull(filter); + return fromCollection.removeIf(element -> filter.test(function.apply(element))); + } + + @Override + public Spliterator spliterator() { + return SpliteratorUtil.trans(fromCollection.spliterator(), function); + } + + @Override + public int size() { + return fromCollection.size(); + } +} \ No newline at end of file diff --git a/src/main/java/cn/hutool/core/collection/TransIter.java b/src/main/java/cn/hutool/core/collection/TransIter.java new file mode 100644 index 0000000..116fd69 --- /dev/null +++ b/src/main/java/cn/hutool/core/collection/TransIter.java @@ -0,0 +1,46 @@ +package cn.hutool.core.collection; + +import cn.hutool.core.lang.Assert; + +import java.util.Iterator; +import java.util.function.Function; + +/** + * 使用给定的转换函数,转换源{@link Iterator}为新类型的{@link Iterator} + * + * @param 源元素类型 + * @param 目标元素类型 + * @author looly + * @since 5.4.3 + */ +public class TransIter implements Iterator { + + private final Iterator backingIterator; + private final Function func; + + /** + * 构造 + * + * @param backingIterator 源{@link Iterator} + * @param func 转换函数 + */ + public TransIter(final Iterator backingIterator, final Function func) { + this.backingIterator = Assert.notNull(backingIterator); + this.func = Assert.notNull(func); + } + + @Override + public final boolean hasNext() { + return backingIterator.hasNext(); + } + + @Override + public final T next() { + return func.apply(backingIterator.next()); + } + + @Override + public final void remove() { + backingIterator.remove(); + } +} diff --git a/src/main/java/cn/hutool/core/collection/TransSpliterator.java b/src/main/java/cn/hutool/core/collection/TransSpliterator.java new file mode 100644 index 0000000..7a9b435 --- /dev/null +++ b/src/main/java/cn/hutool/core/collection/TransSpliterator.java @@ -0,0 +1,51 @@ +package cn.hutool.core.collection; + +import java.util.Spliterator; +import java.util.function.Consumer; +import java.util.function.Function; + +/** + * 使用给定的转换函数,转换源{@link Spliterator}为新类型的{@link Spliterator} + * + * @param 源元素类型 + * @param 目标元素类型 + * @author looly + * @since 5.4.3 + */ +public class TransSpliterator implements Spliterator { + private final Spliterator fromSpliterator; + private final Function function; + + public TransSpliterator(Spliterator fromSpliterator, Function function) { + this.fromSpliterator = fromSpliterator; + this.function = function; + } + + @Override + public boolean tryAdvance(Consumer action) { + return fromSpliterator.tryAdvance( + fromElement -> action.accept(function.apply(fromElement))); + } + + @Override + public void forEachRemaining(Consumer action) { + fromSpliterator.forEachRemaining(fromElement -> action.accept(function.apply(fromElement))); + } + + @Override + public Spliterator trySplit() { + Spliterator fromSplit = fromSpliterator.trySplit(); + return (fromSplit != null) ? new TransSpliterator<>(fromSplit, function) : null; + } + + @Override + public long estimateSize() { + return fromSpliterator.estimateSize(); + } + + @Override + public int characteristics() { + return fromSpliterator.characteristics() + & ~(Spliterator.DISTINCT | Spliterator.NONNULL | Spliterator.SORTED); + } +} diff --git a/src/main/java/cn/hutool/core/collection/UniqueKeySet.java b/src/main/java/cn/hutool/core/collection/UniqueKeySet.java new file mode 100644 index 0000000..8f2aa4f --- /dev/null +++ b/src/main/java/cn/hutool/core/collection/UniqueKeySet.java @@ -0,0 +1,177 @@ +package cn.hutool.core.collection; + +import cn.hutool.core.map.MapBuilder; +import cn.hutool.core.util.ObjectUtil; + +import java.io.Serializable; +import java.util.AbstractSet; +import java.util.Collection; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; +import java.util.function.Function; + +/** + * 唯一键的Set
+ * 通过自定义唯一键,通过{@link #uniqueGenerator}生成节点对象对应的键作为Map的key,确定唯一
+ * 此Set与HashSet不同的是,HashSet依赖于{@link Object#equals(Object)}确定唯一
+ * 但是很多时候我们无法对对象进行修改,此时在外部定义一个唯一规则,即可完成去重。 + *
+ * {@code Set set = new UniqueKeySet<>(UniqueTestBean::getId);}
+ * 
+ * + * @param 唯一键类型 + * @param 值对象 + * @author looly + * @since 5.7.23 + */ +public class UniqueKeySet extends AbstractSet implements Serializable { + private static final long serialVersionUID = 1L; + + private Map map; + private final Function uniqueGenerator; + + //region 构造 + + /** + * 构造 + * + * @param uniqueGenerator 唯一键生成规则函数,用于生成对象对应的唯一键 + */ + public UniqueKeySet(Function uniqueGenerator) { + this(false, uniqueGenerator); + } + + /** + * 构造 + * + * @param uniqueGenerator 唯一键生成规则函数,用于生成对象对应的唯一键 + * @param c 初始化加入的集合 + * @since 5.8.0 + */ + public UniqueKeySet(Function uniqueGenerator, Collection c) { + this(false, uniqueGenerator, c); + } + + /** + * 构造 + * + * @param isLinked 是否保持加入顺序 + * @param uniqueGenerator 唯一键生成规则函数,用于生成对象对应的唯一键 + */ + public UniqueKeySet(boolean isLinked, Function uniqueGenerator) { + this(MapBuilder.create(isLinked), uniqueGenerator); + } + + /** + * 构造 + * + * @param isLinked 是否保持加入顺序 + * @param uniqueGenerator 唯一键生成规则函数,用于生成对象对应的唯一键 + * @param c 初始化加入的集合 + * @since 5.8.0 + */ + public UniqueKeySet(boolean isLinked, Function uniqueGenerator, Collection c) { + this(isLinked, uniqueGenerator); + addAll(c); + } + + /** + * 构造 + * + * @param initialCapacity 初始容量 + * @param loadFactor 增长因子 + * @param uniqueGenerator 唯一键生成规则函数,用于生成对象对应的唯一键 + */ + public UniqueKeySet(int initialCapacity, float loadFactor, Function uniqueGenerator) { + this(MapBuilder.create(new HashMap<>(initialCapacity, loadFactor)), uniqueGenerator); + } + + /** + * 构造 + * + * @param builder 初始Map,定义了Map类型 + * @param uniqueGenerator 唯一键生成规则函数,用于生成对象对应的唯一键 + */ + public UniqueKeySet(MapBuilder builder, Function uniqueGenerator) { + this.map = builder.build(); + this.uniqueGenerator = uniqueGenerator; + } + + //endregion + + @Override + public Iterator iterator() { + return map.values().iterator(); + } + + @Override + public int size() { + return map.size(); + } + + @Override + public boolean isEmpty() { + return map.isEmpty(); + } + + @Override + public boolean contains(Object o) { + //noinspection unchecked + return map.containsKey(this.uniqueGenerator.apply((V) o)); + } + + @Override + public boolean add(V v) { + return null == map.put(this.uniqueGenerator.apply(v), v); + } + + /** + * 加入值,如果值已经存在,则忽略之 + * + * @param v 值 + * @return 是否成功加入 + */ + public boolean addIfAbsent(V v) { + return null == map.putIfAbsent(this.uniqueGenerator.apply(v), v); + } + + /** + * 加入集合中所有的值,如果值已经存在,则忽略之 + * + * @param c 集合 + * @return 是否有一个或多个被加入成功 + */ + public boolean addAllIfAbsent(Collection c) { + boolean modified = false; + for (V v : c) + if (addIfAbsent(v)) { + modified = true; + } + return modified; + } + + @Override + public boolean remove(Object o) { + //noinspection unchecked + return null != map.remove(this.uniqueGenerator.apply((V) o)); + } + + @Override + public void clear() { + map.clear(); + } + + @Override + @SuppressWarnings("unchecked") + public UniqueKeySet clone() { + try { + UniqueKeySet newSet = (UniqueKeySet) super.clone(); + newSet.map = ObjectUtil.clone(this.map); + return newSet; + } catch (CloneNotSupportedException e) { + throw new InternalError(e); + } + } + +} diff --git a/src/main/java/cn/hutool/core/collection/package-info.java b/src/main/java/cn/hutool/core/collection/package-info.java new file mode 100644 index 0000000..bf8e234 --- /dev/null +++ b/src/main/java/cn/hutool/core/collection/package-info.java @@ -0,0 +1,7 @@ +/** + * 集合以及Iterator封装,包括集合工具CollUtil,Iterator和Iterable工具IterUtil + * + * @author looly + * + */ +package cn.hutool.core.collection; \ No newline at end of file diff --git a/src/main/java/cn/hutool/core/comparator/BaseFieldComparator.java b/src/main/java/cn/hutool/core/comparator/BaseFieldComparator.java new file mode 100644 index 0000000..0888aea --- /dev/null +++ b/src/main/java/cn/hutool/core/comparator/BaseFieldComparator.java @@ -0,0 +1,60 @@ +package cn.hutool.core.comparator; + +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.ReflectUtil; + +import java.io.Serializable; +import java.lang.reflect.Field; +import java.util.Comparator; + +/** + * Bean字段排序器
+ * 参阅feilong-core中的PropertyComparator + * + * @param 被比较的Bean + * @author jiangzeyin + * @deprecated 此类不再需要,使用FuncComparator代替更加灵活 + */ +@Deprecated +public abstract class BaseFieldComparator implements Comparator, Serializable { + private static final long serialVersionUID = -3482464782340308755L; + + /** + * 比较两个对象的同一个字段值 + * + * @param o1 对象1 + * @param o2 对象2 + * @param field 字段 + * @return 比较结果 + */ + protected int compareItem(T o1, T o2, Field field) { + if (o1 == o2) { + return 0; + } else if (null == o1) {// null 排在后面 + return 1; + } else if (null == o2) { + return -1; + } + + Comparable v1; + Comparable v2; + try { + v1 = (Comparable) ReflectUtil.getFieldValue(o1, field); + v2 = (Comparable) ReflectUtil.getFieldValue(o2, field); + } catch (Exception e) { + throw new ComparatorException(e); + } + + return compare(o1, o2, v1, v2); + } + + @SuppressWarnings({"rawtypes", "unchecked"}) + private int compare(T o1, T o2, Comparable fieldValue1, Comparable fieldValue2) { + int result = ObjectUtil.compare(fieldValue1, fieldValue2); + if (0 == result) { + //避免TreeSet / TreeMap 过滤掉排序字段相同但是对象不相同的情况 + result = CompareUtil.compare(o1, o2, true); + } + return result; + } +} diff --git a/src/main/java/cn/hutool/core/comparator/ComparableComparator.java b/src/main/java/cn/hutool/core/comparator/ComparableComparator.java new file mode 100644 index 0000000..a531878 --- /dev/null +++ b/src/main/java/cn/hutool/core/comparator/ComparableComparator.java @@ -0,0 +1,53 @@ +package cn.hutool.core.comparator; + +import java.io.Serializable; +import java.util.Comparator; + +/** + * 针对 {@link Comparable}对象的默认比较器 + * + * @param 比较对象类型 + * @author Looly + * @since 3.0.7 + */ +public class ComparableComparator> implements Comparator, Serializable { + private static final long serialVersionUID = 3020871676147289162L; + + /** 单例 */ + @SuppressWarnings("rawtypes") + public static final ComparableComparator INSTANCE = new ComparableComparator<>(); + + /** + * 构造 + */ + public ComparableComparator() { + } + + /** + * 比较两个{@link Comparable}对象 + * + *
+	 * obj1.compareTo(obj2)
+	 * 
+ * + * @param obj1 被比较的第一个对象 + * @param obj2 the second object to compare + * @return obj1小返回负数,大返回正数,否则返回0 + * @throws NullPointerException obj1为{@code null}或者比较中抛出空指针异常 + */ + @Override + public int compare(final E obj1, final E obj2) { + return obj1.compareTo(obj2); + } + + @Override + public int hashCode() { + return "ComparableComparator".hashCode(); + } + + @Override + public boolean equals(final Object object) { + return this == object || null != object && object.getClass().equals(this.getClass()); + } + +} \ No newline at end of file diff --git a/src/main/java/cn/hutool/core/comparator/ComparatorChain.java b/src/main/java/cn/hutool/core/comparator/ComparatorChain.java new file mode 100644 index 0000000..d3d5127 --- /dev/null +++ b/src/main/java/cn/hutool/core/comparator/ComparatorChain.java @@ -0,0 +1,356 @@ +package cn.hutool.core.comparator; + +import cn.hutool.core.lang.Chain; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.BitSet; +import java.util.Comparator; +import java.util.Iterator; +import java.util.List; +import java.util.Objects; + +/** + * 比较器链。此链包装了多个比较器,最终比较结果按照比较器顺序综合多个比较器结果。
+ * 按照比较器链的顺序分别比较,如果比较出相等则转向下一个比较器,否则直接返回
+ * 此类copy from Apache-commons-collections + * + * @author looly + * @since 3.0.7 + */ +public class ComparatorChain implements Chain, ComparatorChain>, Comparator, Serializable { + private static final long serialVersionUID = -2426725788913962429L; + + /** + * 比较器链. + */ + private final List> chain; + /** + * 对应比较器位置是否反序. + */ + private final BitSet orderingBits; + /** + * 比较器是否被锁定。锁定的比较器链不能再添加新的比较器。比较器会在开始比较时开始加锁。 + */ + private boolean lock = false; + + //------------------------------------------------------------------------------------- Static method start + + /** + * 构建 {@link ComparatorChain} + * + * @param 被比较对象类型 + * @param comparator 比较器 + * @return {@link ComparatorChain} + * @since 5.4.3 + */ + public static ComparatorChain of(Comparator comparator) { + return of(comparator, false); + } + + /** + * 构建 {@link ComparatorChain} + * + * @param 被比较对象类型 + * @param comparator 比较器 + * @param reverse 是否反向 + * @return {@link ComparatorChain} + * @since 5.4.3 + */ + public static ComparatorChain of(Comparator comparator, boolean reverse) { + return new ComparatorChain<>(comparator, reverse); + } + + /** + * 构建 {@link ComparatorChain} + * + * @param 被比较对象类型 + * @param comparators 比较器数组 + * @return {@link ComparatorChain} + * @since 5.4.3 + */ + @SafeVarargs + public static ComparatorChain of(Comparator... comparators) { + return of(Arrays.asList(comparators)); + } + + /** + * 构建 {@link ComparatorChain} + * + * @param 被比较对象类型 + * @param comparators 比较器列表 + * @return {@link ComparatorChain} + * @since 5.4.3 + */ + public static ComparatorChain of(List> comparators) { + return new ComparatorChain<>(comparators); + } + + /** + * 构建 {@link ComparatorChain} + * + * @param 被比较对象类型 + * @param comparators 比较器列表 + * @param bits {@link Comparator} 列表对应的排序boolean值,true表示正序,false反序 + * @return {@link ComparatorChain} + * @since 5.4.3 + */ + public static ComparatorChain of(List> comparators, BitSet bits) { + return new ComparatorChain<>(comparators, bits); + } + //------------------------------------------------------------------------------------- Static method start + + /** + * 构造空的比较器链,必须至少有一个比较器,否则会在compare时抛出{@link UnsupportedOperationException} + */ + public ComparatorChain() { + this(new ArrayList<>(), new BitSet()); + } + + /** + * 构造,初始化单一比较器。比较器为正序 + * + * @param comparator 在比较器链中的第一个比较器 + */ + public ComparatorChain(final Comparator comparator) { + this(comparator, false); + } + + /** + * 构造,初始化单一比较器。自定义正序还是反序 + * + * @param comparator 在比较器链中的第一个比较器 + * @param reverse 是否反序,true表示反序,false正序 + */ + public ComparatorChain(final Comparator comparator, final boolean reverse) { + chain = new ArrayList<>(1); + chain.add(comparator); + orderingBits = new BitSet(1); + if (reverse) { + orderingBits.set(0); + } + } + + /** + * 构造,使用已有的比较器列表 + * + * @param list 比较器列表 + * @see #ComparatorChain(List, BitSet) + */ + public ComparatorChain(final List> list) { + this(list, new BitSet(list.size())); + } + + /** + * 构造,使用已有的比较器列表和对应的BitSet
+ * BitSet中的boolean值需与list中的{@link Comparator}一一对应,true表示正序,false反序 + * + * @param list {@link Comparator} 列表 + * @param bits {@link Comparator} 列表对应的排序boolean值,true表示正序,false反序 + */ + public ComparatorChain(final List> list, final BitSet bits) { + chain = list; + orderingBits = bits; + } + + /** + * 在链的尾部添加比较器,使用正向排序 + * + * @param comparator {@link Comparator} 比较器,正向 + * @return this + */ + public ComparatorChain addComparator(final Comparator comparator) { + return addComparator(comparator, false); + } + + /** + * 在链的尾部添加比较器,使用给定排序方式 + * + * @param comparator {@link Comparator} 比较器 + * @param reverse 是否反序,true表示正序,false反序 + * @return this + */ + public ComparatorChain addComparator(final Comparator comparator, final boolean reverse) { + checkLocked(); + + chain.add(comparator); + if (reverse) { + orderingBits.set(chain.size() - 1); + } + return this; + } + + /** + * 替换指定位置的比较器,保持原排序方式 + * + * @param index 位置 + * @param comparator {@link Comparator} + * @return this + * @throws IndexOutOfBoundsException if index < 0 or index >= size() + */ + public ComparatorChain setComparator(final int index, final Comparator comparator) throws IndexOutOfBoundsException { + return setComparator(index, comparator, false); + } + + /** + * 替换指定位置的比较器,替换指定排序方式 + * + * @param index 位置 + * @param comparator {@link Comparator} + * @param reverse 是否反序,true表示正序,false反序 + * @return this + */ + public ComparatorChain setComparator(final int index, final Comparator comparator, final boolean reverse) { + checkLocked(); + + chain.set(index, comparator); + if (reverse) { + orderingBits.set(index); + } else { + orderingBits.clear(index); + } + return this; + } + + /** + * 更改指定位置的排序方式为正序 + * + * @param index 位置 + * @return this + */ + public ComparatorChain setForwardSort(final int index) { + checkLocked(); + orderingBits.clear(index); + return this; + } + + /** + * 更改指定位置的排序方式为反序 + * + * @param index 位置 + * @return this + */ + public ComparatorChain setReverseSort(final int index) { + checkLocked(); + orderingBits.set(index); + return this; + } + + /** + * 比较器链中比较器个数 + * + * @return Comparator count + */ + public int size() { + return chain.size(); + } + + /** + * 是否已经被锁定。当开始比较时(调用compare方法)此值为true + * + * @return true = ComparatorChain cannot be modified; false = ComparatorChain can still be modified. + */ + public boolean isLocked() { + return lock; + } + + @Override + public Iterator> iterator() { + return this.chain.iterator(); + } + + @Override + public ComparatorChain addChain(Comparator element) { + return this.addComparator(element); + } + + /** + * 执行比较
+ * 按照比较器链的顺序分别比较,如果比较出相等则转向下一个比较器,否则直接返回 + * + * @param o1 第一个对象 + * @param o2 第二个对象 + * @return -1, 0, or 1 + * @throws UnsupportedOperationException 如果比较器链为空,无法完成比较 + */ + @Override + public int compare(final E o1, final E o2) throws UnsupportedOperationException { + if (!lock) { + checkChainIntegrity(); + lock = true; + } + + final Iterator> comparators = chain.iterator(); + Comparator comparator; + int retval; + for (int comparatorIndex = 0; comparators.hasNext(); ++comparatorIndex) { + comparator = comparators.next(); + retval = comparator.compare(o1, o2); + if (retval != 0) { + // invert the order if it is a reverse sort + if (orderingBits.get(comparatorIndex)) { + retval = (retval > 0) ? -1 : 1; + } + return retval; + } + } + + // if comparators are exhausted, return 0 + return 0; + } + + @Override + public int hashCode() { + int hash = 0; + if (null != chain) { + hash ^= chain.hashCode(); + } + if (null != orderingBits) { + hash ^= orderingBits.hashCode(); + } + return hash; + } + + @Override + public boolean equals(final Object object) { + if (this == object) { + return true; + } + if (null == object) { + return false; + } + if (object.getClass().equals(this.getClass())) { + final ComparatorChain otherChain = (ComparatorChain) object; + // + return Objects.equals(this.orderingBits, otherChain.orderingBits) + && this.chain.equals(otherChain.chain); + } + return false; + } + + //------------------------------------------------------------------------------------------------------------------------------- Private method start + + /** + * 被锁定时抛出异常 + * + * @throws UnsupportedOperationException 被锁定抛出此异常 + */ + private void checkLocked() { + if (lock) { + throw new UnsupportedOperationException("Comparator ordering cannot be changed after the first comparison is performed"); + } + } + + /** + * 检查比较器链是否为空,为空抛出异常 + * + * @throws UnsupportedOperationException 为空抛出此异常 + */ + private void checkChainIntegrity() { + if (chain.size() == 0) { + throw new UnsupportedOperationException("ComparatorChains must contain at least one Comparator"); + } + } + //------------------------------------------------------------------------------------------------------------------------------- Private method start +} diff --git a/src/main/java/cn/hutool/core/comparator/ComparatorException.java b/src/main/java/cn/hutool/core/comparator/ComparatorException.java new file mode 100644 index 0000000..a21670f --- /dev/null +++ b/src/main/java/cn/hutool/core/comparator/ComparatorException.java @@ -0,0 +1,32 @@ +package cn.hutool.core.comparator; + +import cn.hutool.core.exceptions.ExceptionUtil; +import cn.hutool.core.util.StrUtil; + +/** + * 比较异常 + * @author xiaoleilu + */ +public class ComparatorException extends RuntimeException{ + private static final long serialVersionUID = 4475602435485521971L; + + public ComparatorException(Throwable e) { + super(ExceptionUtil.getMessage(e), e); + } + + public ComparatorException(String message) { + super(message); + } + + public ComparatorException(String messageTemplate, Object... params) { + super(StrUtil.format(messageTemplate, params)); + } + + public ComparatorException(String message, Throwable throwable) { + super(message, throwable); + } + + public ComparatorException(Throwable throwable, String messageTemplate, Object... params) { + super(StrUtil.format(messageTemplate, params), throwable); + } +} diff --git a/src/main/java/cn/hutool/core/comparator/CompareUtil.java b/src/main/java/cn/hutool/core/comparator/CompareUtil.java new file mode 100644 index 0000000..8a93c1e --- /dev/null +++ b/src/main/java/cn/hutool/core/comparator/CompareUtil.java @@ -0,0 +1,190 @@ +package cn.hutool.core.comparator; + +import java.util.Comparator; +import java.util.Objects; +import java.util.function.Function; + +/** + * 比较工具类 + * + * @author looly + */ +public class CompareUtil { + + /** + * 获取自然排序器,即默认排序器 + * + * @param 排序节点类型 + * @return 默认排序器 + * @since 5.7.21 + */ + @SuppressWarnings("unchecked") + public static > Comparator naturalComparator() { + return ComparableComparator.INSTANCE; + } + + /** + * 对象比较,比较结果取决于comparator,如果被比较对象为null,传入的comparator对象应处理此情况
+ * 如果传入comparator为null,则使用默认规则比较(此时被比较对象必须实现Comparable接口) + * + *

+ * 一般而言,如果c1 < c2,返回数小于0,c1==c2返回0,c1 > c2 大于0 + * + * @param 被比较对象类型 + * @param c1 对象1 + * @param c2 对象2 + * @param comparator 比较器 + * @return 比较结果 + * @see Comparator#compare(Object, Object) + * @since 4.6.9 + */ + @SuppressWarnings({"rawtypes", "unchecked"}) + public static int compare(T c1, T c2, Comparator comparator) { + if (null == comparator) { + return compare((Comparable) c1, (Comparable) c2); + } + return comparator.compare(c1, c2); + } + + /** + * {@code null}安全的对象比较,{@code null}对象小于任何对象 + * + * @param 被比较对象类型 + * @param c1 对象1,可以为{@code null} + * @param c2 对象2,可以为{@code null} + * @return 比较结果,如果c1 < c2,返回数小于0,c1==c2返回0,c1 > c2 大于0 + * @see Comparator#compare(Object, Object) + */ + public static > int compare(T c1, T c2) { + return compare(c1, c2, false); + } + + /** + * {@code null}安全的对象比较 + * + * @param 被比较对象类型(必须实现Comparable接口) + * @param c1 对象1,可以为{@code null} + * @param c2 对象2,可以为{@code null} + * @param isNullGreater 当被比较对象为null时是否排在后面,true表示null大于任何对象,false反之 + * @return 比较结果,如果c1 < c2,返回数小于0,c1==c2返回0,c1 > c2 大于0 + * @see Comparator#compare(Object, Object) + */ + public static > int compare(T c1, T c2, boolean isNullGreater) { + if (c1 == c2) { + return 0; + } else if (c1 == null) { + return isNullGreater ? 1 : -1; + } else if (c2 == null) { + return isNullGreater ? -1 : 1; + } + return c1.compareTo(c2); + } + + /** + * 自然比较两个对象的大小,比较规则如下: + * + *

+	 * 1、如果实现Comparable调用compareTo比较
+	 * 2、o1.equals(o2)返回0
+	 * 3、比较hashCode值
+	 * 4、比较toString值
+	 * 
+ * + * @param 被比较对象类型 + * @param o1 对象1 + * @param o2 对象2 + * @param isNullGreater null值是否做为最大值 + * @return 比较结果,如果o1 < o2,返回数小于0,o1==o2返回0,o1 > o2 大于0 + */ + @SuppressWarnings({"unchecked", "rawtypes"}) + public static int compare(T o1, T o2, boolean isNullGreater) { + if (o1 == o2) { + return 0; + } else if (null == o1) {// null 排在后面 + return isNullGreater ? 1 : -1; + } else if (null == o2) { + return isNullGreater ? -1 : 1; + } + + if (o1 instanceof Comparable && o2 instanceof Comparable) { + //如果bean可比较,直接比较bean + return ((Comparable) o1).compareTo(o2); + } + + if (o1.equals(o2)) { + return 0; + } + + int result = Integer.compare(o1.hashCode(), o2.hashCode()); + if (0 == result) { + result = compare(o1.toString(), o2.toString()); + } + + return result; + } + + /** + * 中文比较器 + * + * @param keyExtractor 从对象中提取中文(参与比较的内容) + * @param 对象类型 + * @return 中文比较器 + * @since 5.4.3 + */ + public static Comparator comparingPinyin(Function keyExtractor) { + return comparingPinyin(keyExtractor, false); + } + + /** + * 中文(拼音)比较器 + * + * @param keyExtractor 从对象中提取中文(参与比较的内容) + * @param reverse 是否反序 + * @param 对象类型 + * @return 中文比较器 + * @since 5.4.3 + */ + public static Comparator comparingPinyin(Function keyExtractor, boolean reverse) { + Objects.requireNonNull(keyExtractor); + PinyinComparator pinyinComparator = new PinyinComparator(); + if (reverse) { + return (o1, o2) -> pinyinComparator.compare(keyExtractor.apply(o2), keyExtractor.apply(o1)); + } + return (o1, o2) -> pinyinComparator.compare(keyExtractor.apply(o1), keyExtractor.apply(o2)); + } + + /** + * 索引比较器
+ * 通过keyExtractor函数,提取对象的某个属性或规则,根据提供的排序数组,完成比较
+ * + * @param keyExtractor 从对象中提取中文(参与比较的内容) + * @param objs 参与排序的数组,数组的元素位置决定了对象的排序先后 + * @param 对象类型 + * @param 数组对象类型 + * @return 索引比较器 + * @since 5.8.0 + */ + @SuppressWarnings("unchecked") + public static Comparator comparingIndexed(Function keyExtractor, U... objs) { + return comparingIndexed(keyExtractor, false, objs); + } + + /** + * 索引比较器
+ * 通过keyExtractor函数,提取对象的某个属性或规则,根据提供的排序数组,完成比较
+ * + * @param keyExtractor 从对象中提取排序键的函数(参与比较的内容) + * @param atEndIfMiss 如果不在列表中是否排在后边 + * @param objs 参与排序的数组,数组的元素位置决定了对象的排序先后 + * @param 对象类型 + * @param 数组对象类型 + * @return 索引比较器 + * @since 5.8.0 + */ + @SuppressWarnings("unchecked") + public static Comparator comparingIndexed(Function keyExtractor, boolean atEndIfMiss, U... objs) { + Objects.requireNonNull(keyExtractor); + IndexedComparator indexedComparator = new IndexedComparator<>(atEndIfMiss, objs); + return (o1, o2) -> indexedComparator.compare(keyExtractor.apply(o1), keyExtractor.apply(o2)); + } +} diff --git a/src/main/java/cn/hutool/core/comparator/FieldComparator.java b/src/main/java/cn/hutool/core/comparator/FieldComparator.java new file mode 100644 index 0000000..b4a1dd1 --- /dev/null +++ b/src/main/java/cn/hutool/core/comparator/FieldComparator.java @@ -0,0 +1,65 @@ +package cn.hutool.core.comparator; + +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.ClassUtil; +import cn.hutool.core.util.ReflectUtil; +import cn.hutool.core.util.StrUtil; + +import java.lang.reflect.Field; + +/** + * Bean字段排序器
+ * 参阅feilong-core中的PropertyComparator + * + * @param 被比较的Bean + * @author Looly + */ +public class FieldComparator extends FuncComparator { + private static final long serialVersionUID = 9157326766723846313L; + + /** + * 构造 + * + * @param beanClass Bean类 + * @param fieldName 字段名 + */ + public FieldComparator(Class beanClass, String fieldName) { + this(getNonNullField(beanClass, fieldName)); + } + + /** + * 构造 + * + * @param field 字段 + */ + public FieldComparator(Field field) { + this(true, field); + } + + /** + * 构造 + * + * @param nullGreater 是否{@code null}在后 + * @param field 字段 + */ + public FieldComparator(boolean nullGreater, Field field) { + super(nullGreater, (bean) -> + (Comparable) ReflectUtil.getFieldValue(bean, + Assert.notNull(field, "Field must be not null!"))); + } + + /** + * 获取字段,附带检查字段不存在的问题。 + * + * @param beanClass Bean类 + * @param fieldName 字段名 + * @return 非null字段 + */ + private static Field getNonNullField(Class beanClass, String fieldName) { + final Field field = ClassUtil.getDeclaredField(beanClass, fieldName); + if (field == null) { + throw new IllegalArgumentException(StrUtil.format("Field [{}] not found in Class [{}]", fieldName, beanClass.getName())); + } + return field; + } +} diff --git a/src/main/java/cn/hutool/core/comparator/FieldsComparator.java b/src/main/java/cn/hutool/core/comparator/FieldsComparator.java new file mode 100644 index 0000000..5c10eda --- /dev/null +++ b/src/main/java/cn/hutool/core/comparator/FieldsComparator.java @@ -0,0 +1,50 @@ +package cn.hutool.core.comparator; + +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.ClassUtil; + +import java.lang.reflect.Field; + +/** + * Bean字段排序器
+ * 参阅feilong-core中的PropertyComparator + * + * @param 被比较的Bean + * @author Looly + */ +public class FieldsComparator extends NullComparator { + private static final long serialVersionUID = 8649196282886500803L; + + /** + * 构造 + * + * @param beanClass Bean类 + * @param fieldNames 多个字段名 + */ + public FieldsComparator(Class beanClass, String... fieldNames) { + this(true, beanClass, fieldNames); + } + + /** + * 构造 + * + * @param nullGreater 是否{@code null}在后 + * @param beanClass Bean类 + * @param fieldNames 多个字段名 + */ + public FieldsComparator(boolean nullGreater, Class beanClass, String... fieldNames) { + super(nullGreater, (a, b) -> { + Field field; + for (String fieldName : fieldNames) { + field = ClassUtil.getDeclaredField(beanClass, fieldName); + Assert.notNull(field, "Field [{}] not found in Class [{}]", fieldName, beanClass.getName()); + final int compare = new FieldComparator<>(field).compare(a, b); + if (0 != compare) { + return compare; + } + } + return 0; + }); + } + +} diff --git a/src/main/java/cn/hutool/core/comparator/FuncComparator.java b/src/main/java/cn/hutool/core/comparator/FuncComparator.java new file mode 100644 index 0000000..78ea098 --- /dev/null +++ b/src/main/java/cn/hutool/core/comparator/FuncComparator.java @@ -0,0 +1,63 @@ +package cn.hutool.core.comparator; + +import cn.hutool.core.util.ObjectUtil; + +import java.util.function.Function; + +/** + * 指定函数排序器 + * + * @param 被比较的对象 + * @author looly + */ +public class FuncComparator extends NullComparator { + private static final long serialVersionUID = 1L; + + private final Function> func; + + /** + * 构造 + * + * @param nullGreater 是否{@code null}在后 + * @param func 比较项获取函数 + */ + public FuncComparator(boolean nullGreater, Function> func) { + super(nullGreater, null); + this.func = func; + } + + @Override + protected int doCompare(T a, T b) { + Comparable v1; + Comparable v2; + try { + v1 = func.apply(a); + v2 = func.apply(b); + } catch (Exception e) { + throw new ComparatorException(e); + } + + return compare(a, b, v1, v2); + } + + /** + * 对象及对应比较的值的综合比较
+ * 考虑到如果对象对应的比较值相同,如对象的字段值相同,则返回相同结果,此时在TreeMap等容器比较去重时会去重。
+ * 因此需要比较下对象本身以避免去重 + * + * @param o1 对象1 + * @param o2 对象2 + * @param v1 被比较的值1 + * @param v2 被比较的值2 + * @return 比较结果 + */ + @SuppressWarnings({"rawtypes", "unchecked"}) + private int compare(T o1, T o2, Comparable v1, Comparable v2) { + int result = ObjectUtil.compare(v1, v2); + if (0 == result) { + //避免TreeSet / TreeMap 过滤掉排序字段相同但是对象不相同的情况 + result = CompareUtil.compare(o1, o2, this.nullGreater); + } + return result; + } +} diff --git a/src/main/java/cn/hutool/core/comparator/IndexedComparator.java b/src/main/java/cn/hutool/core/comparator/IndexedComparator.java new file mode 100644 index 0000000..d54cd3c --- /dev/null +++ b/src/main/java/cn/hutool/core/comparator/IndexedComparator.java @@ -0,0 +1,75 @@ +package cn.hutool.core.comparator; + +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.ArrayUtil; + +import java.util.Comparator; + +/** + * 按照数组的顺序正序排列,数组的元素位置决定了对象的排序先后
+ * 默认的,如果参与排序的元素并不在数组中,则排序在前(可以通过atEndIfMiss设置) + * + * @param 被排序元素类型 + * @author looly + * @since 4.1.5 + */ +public class IndexedComparator implements Comparator { + + private final boolean atEndIfMiss; + private final T[] array; + + /** + * 构造 + * + * @param objs 参与排序的数组,数组的元素位置决定了对象的排序先后 + */ + @SuppressWarnings("unchecked") + public IndexedComparator(T... objs) { + this(false, objs); + } + + /** + * 构造 + * + * @param atEndIfMiss 如果不在列表中是否排在后边 + * @param objs 参与排序的数组,数组的元素位置决定了对象的排序先后 + */ + @SuppressWarnings("unchecked") + public IndexedComparator(boolean atEndIfMiss, T... objs) { + Assert.notNull(objs, "'objs' array must not be null"); + this.atEndIfMiss = atEndIfMiss; + this.array = objs; + } + + @Override + public int compare(T o1, T o2) { + final int index1 = getOrder(o1); + final int index2 = getOrder(o2); + + if (index1 == index2) { + if (index1 < 0 || index1 == this.array.length) { + // 任意一个元素不在列表中, 返回原顺序 + return 1; + } + + // 位置一样,认为是同一个元素 + return 0; + } + + return Integer.compare(index1, index2); + } + + /** + * 查找对象类型所在列表的位置 + * + * @param object 对象 + * @return 位置,未找到位置根据{@link #atEndIfMiss}取不同值,false返回-1,否则返回列表长度 + */ + private int getOrder(T object) { + int order = ArrayUtil.indexOf(array, object); + if (order < 0) { + order = this.atEndIfMiss ? this.array.length : -1; + } + return order; + } +} diff --git a/src/main/java/cn/hutool/core/comparator/InstanceComparator.java b/src/main/java/cn/hutool/core/comparator/InstanceComparator.java new file mode 100644 index 0000000..321056f --- /dev/null +++ b/src/main/java/cn/hutool/core/comparator/InstanceComparator.java @@ -0,0 +1,83 @@ +/* + * Copyright 2002-2019 the original author or authors. + * + * Licensed 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 + * + * https://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 cn.hutool.core.comparator; + +import cn.hutool.core.lang.Assert; + +import java.util.Comparator; + +/** + * 按照指定类型顺序排序,对象顺序取决于对象对应的类在数组中的位置。 + * + *

如果对比的两个对象类型相同,返回{@code 0},默认如果对象类型不在列表中,则排序在前

+ *

此类来自Spring,有所改造

+ * + * @param 用于比较的对象类型 + * @author Phillip Webb + * @since 5.4.1 + */ +public class InstanceComparator implements Comparator { + + private final boolean atEndIfMiss; + private final Class[] instanceOrder; + + /** + * 构造 + * + * @param instanceOrder 用于比较排序的对象类型数组,排序按照数组位置排序 + */ + public InstanceComparator(Class... instanceOrder) { + this(false, instanceOrder); + } + + /** + * 构造 + * + * @param atEndIfMiss 如果不在列表中是否排在后边 + * @param instanceOrder 用于比较排序的对象类型数组,排序按照数组位置排序 + */ + public InstanceComparator(boolean atEndIfMiss, Class... instanceOrder) { + Assert.notNull(instanceOrder, "'instanceOrder' array must not be null"); + this.atEndIfMiss = atEndIfMiss; + this.instanceOrder = instanceOrder; + } + + + @Override + public int compare(T o1, T o2) { + int i1 = getOrder(o1); + int i2 = getOrder(o2); + return Integer.compare(i1, i2); + } + + /** + * 查找对象类型所在列表的位置 + * + * @param object 对象 + * @return 位置,未找到位置根据{@link #atEndIfMiss}取不同值,false返回-1,否则返回列表长度 + */ + private int getOrder(T object) { + if (object != null) { + for (int i = 0; i < this.instanceOrder.length; i++) { + if (this.instanceOrder[i].isInstance(object)) { + return i; + } + } + } + return this.atEndIfMiss ? this.instanceOrder.length : -1; + } +} diff --git a/src/main/java/cn/hutool/core/comparator/LengthComparator.java b/src/main/java/cn/hutool/core/comparator/LengthComparator.java new file mode 100644 index 0000000..0c8f0ea --- /dev/null +++ b/src/main/java/cn/hutool/core/comparator/LengthComparator.java @@ -0,0 +1,25 @@ +package cn.hutool.core.comparator; + +import java.util.Comparator; + +/** + * 字符串长度比较器,短在前 + * + * @author looly + * @since 5.8.9 + */ +public class LengthComparator implements Comparator { + /** + * 单例的字符串长度比较器,短在前 + */ + public static final LengthComparator INSTANCE = new LengthComparator(); + + @Override + public int compare(CharSequence o1, CharSequence o2) { + int result = Integer.compare(o1.length(), o2.length()); + if (0 == result) { + result = CompareUtil.compare(o1.toString(), o2.toString()); + } + return result; + } +} diff --git a/src/main/java/cn/hutool/core/comparator/NullComparator.java b/src/main/java/cn/hutool/core/comparator/NullComparator.java new file mode 100644 index 0000000..877cde4 --- /dev/null +++ b/src/main/java/cn/hutool/core/comparator/NullComparator.java @@ -0,0 +1,78 @@ +package cn.hutool.core.comparator; + +import java.io.Serializable; +import java.util.Comparator; +import java.util.Objects; + +/** + * {@code null}友好的比较器包装,如果nullGreater,则{@code null} > non-null,否则反之。
+ * 如果二者皆为{@code null},则为相等,返回0。
+ * 如果二者都非{@code null},则使用传入的比较器排序。
+ * 传入比较器为{@code null},则看被比较的两个对象是否都实现了{@link Comparable}实现则调用{@link Comparable#compareTo(Object)}。 + * 如果两者至少一个未实现,则视为所有元素相等。 + * + * @param 被比较的对象 + * @author looly + * @since 5.7.10 + */ +public class NullComparator implements Comparator, Serializable { + private static final long serialVersionUID = 1L; + + protected final boolean nullGreater; + protected final Comparator comparator; + + /** + * 构造 + * @param nullGreater 是否{@code null}最大,排在最后 + * @param comparator 实际比较器 + */ + @SuppressWarnings("unchecked") + public NullComparator(boolean nullGreater, Comparator comparator) { + this.nullGreater = nullGreater; + this.comparator = (Comparator) comparator; + } + + @Override + public int compare(T a, T b) { + if (a == b) { + return 0; + }if (a == null) { + return nullGreater ? 1 : -1; + } else if (b == null) { + return nullGreater ? -1 : 1; + } else { + return doCompare(a, b); + } + } + + @Override + public Comparator thenComparing(Comparator other) { + Objects.requireNonNull(other); + return new NullComparator<>(nullGreater, comparator == null ? other : comparator.thenComparing(other)); + } + + @Override + public Comparator reversed() { + return new NullComparator<>((!nullGreater), comparator == null ? null : comparator.reversed()); + } + + /** + * 不检查{@code null}的比较方法
+ * 用户可自行重写此方法自定义比较方式 + * + * @param a A值 + * @param b B值 + * @return 比较结果,-1:a小于b,0:相等,1:a大于b + */ + @SuppressWarnings({"rawtypes", "unchecked"}) + protected int doCompare(T a, T b) { + if (null == comparator) { + if (a instanceof Comparable && b instanceof Comparable) { + return ((Comparable) a).compareTo(b); + } + return 0; + } + + return comparator.compare(a, b); + } +} diff --git a/src/main/java/cn/hutool/core/comparator/PinyinComparator.java b/src/main/java/cn/hutool/core/comparator/PinyinComparator.java new file mode 100644 index 0000000..31525f4 --- /dev/null +++ b/src/main/java/cn/hutool/core/comparator/PinyinComparator.java @@ -0,0 +1,31 @@ +package cn.hutool.core.comparator; + +import java.io.Serializable; +import java.text.Collator; +import java.util.Comparator; +import java.util.Locale; + +/** + * 按照GBK拼音顺序对给定的汉字字符串排序 + * + * @author looly + * @since 4.0.8 + */ +public class PinyinComparator implements Comparator, Serializable { + private static final long serialVersionUID = 1L; + + final Collator collator; + + /** + * 构造 + */ + public PinyinComparator() { + collator = Collator.getInstance(Locale.CHINESE); + } + + @Override + public int compare(String o1, String o2) { + return collator.compare(o1, o2); + } + +} diff --git a/src/main/java/cn/hutool/core/comparator/PropertyComparator.java b/src/main/java/cn/hutool/core/comparator/PropertyComparator.java new file mode 100644 index 0000000..c94ced2 --- /dev/null +++ b/src/main/java/cn/hutool/core/comparator/PropertyComparator.java @@ -0,0 +1,34 @@ +package cn.hutool.core.comparator; + +import cn.hutool.core.bean.BeanUtil; + +/** + * Bean属性排序器
+ * 支持读取Bean多层次下的属性 + * + * @author Looly + * + * @param 被比较的Bean + */ +public class PropertyComparator extends FuncComparator { + private static final long serialVersionUID = 9157326766723846313L; + + /** + * 构造 + * + * @param property 属性名 + */ + public PropertyComparator(String property) { + this(property, true); + } + + /** + * 构造 + * + * @param property 属性名 + * @param isNullGreater null值是否排在后(从小到大排序) + */ + public PropertyComparator(String property, boolean isNullGreater) { + super(isNullGreater, (bean)-> BeanUtil.getProperty(bean, property)); + } +} diff --git a/src/main/java/cn/hutool/core/comparator/ReverseComparator.java b/src/main/java/cn/hutool/core/comparator/ReverseComparator.java new file mode 100644 index 0000000..8535441 --- /dev/null +++ b/src/main/java/cn/hutool/core/comparator/ReverseComparator.java @@ -0,0 +1,49 @@ +package cn.hutool.core.comparator; + +import java.io.Serializable; +import java.util.Comparator; + +/** + * 反转比较器 + * + * @author Looly + * + * @param 被比较对象类型 + */ +public class ReverseComparator implements Comparator, Serializable { + private static final long serialVersionUID = 8083701245147495562L; + + /** 原始比较器 */ + private final Comparator comparator; + + @SuppressWarnings("unchecked") + public ReverseComparator(Comparator comparator) { + this.comparator = (null == comparator) ? ComparableComparator.INSTANCE : comparator; + } + + //----------------------------------------------------------------------------------------------------- + @Override + public int compare(E o1, E o2) { + return comparator.compare(o2, o1); + } + + @Override + public int hashCode() { + return "ReverseComparator".hashCode() ^ comparator.hashCode(); + } + + @Override + public boolean equals(final Object object) { + if (this == object) { + return true; + } + if (null == object) { + return false; + } + if (object.getClass().equals(this.getClass())) { + final ReverseComparator thatrc = (ReverseComparator) object; + return comparator.equals(thatrc.comparator); + } + return false; + } +} diff --git a/src/main/java/cn/hutool/core/comparator/VersionComparator.java b/src/main/java/cn/hutool/core/comparator/VersionComparator.java new file mode 100644 index 0000000..834123c --- /dev/null +++ b/src/main/java/cn/hutool/core/comparator/VersionComparator.java @@ -0,0 +1,88 @@ +package cn.hutool.core.comparator; + +import cn.hutool.core.util.CharUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; + +import java.io.Serializable; +import java.util.Comparator; +import java.util.List; + +/** + * 版本比较器
+ * 比较两个版本的大小
+ * 排序时版本从小到大排序,即比较时小版本在前,大版本在后
+ * 支持如:1.3.20.8,6.82.20160101,8.5a/8.5c等版本形式
+ * 参考:https://www.cnblogs.com/shihaiming/p/6286575.html + * + * @author Looly + * @since 4.0.2 + */ +public class VersionComparator implements Comparator, Serializable { + private static final long serialVersionUID = 8083701245147495562L; + + /** 单例 */ + public static final VersionComparator INSTANCE = new VersionComparator(); + + /** + * 默认构造 + */ + public VersionComparator() { + } + + // ----------------------------------------------------------------------------------------------------- + /** + * 比较两个版本
+ * null版本排在最小:即: + *
+	 * compare(null, "v1") < 0
+	 * compare("v1", "v1")  = 0
+	 * compare(null, null)   = 0
+	 * compare("v1", null) > 0
+	 * compare("1.0.0", "1.0.2") < 0
+	 * compare("1.0.2", "1.0.2a") < 0
+	 * compare("1.13.0", "1.12.1c") > 0
+	 * compare("V0.0.20170102", "V0.0.20170101") > 0
+	 * 
+ * + * @param version1 版本1 + * @param version2 版本2 + */ + @Override + public int compare(String version1, String version2) { + if(ObjectUtil.equal(version1, version2)) { + return 0; + } + if (version1 == null && version2 == null) { + return 0; + } else if (version1 == null) {// null视为最小版本,排在前 + return -1; + } else if (version2 == null) { + return 1; + } + + final List v1s = StrUtil.split(version1, CharUtil.DOT); + final List v2s = StrUtil.split(version2, CharUtil.DOT); + + int diff = 0; + int minLength = Math.min(v1s.size(), v2s.size());// 取最小长度值 + String v1; + String v2; + for (int i = 0; i < minLength; i++) { + v1 = v1s.get(i); + v2 = v2s.get(i); + // 先比较长度 + diff = v1.length() - v2.length(); + if (0 == diff) { + diff = v1.compareTo(v2); + } + if(diff != 0) { + //已有结果,结束 + break; + } + } + + // 如果已经分出大小,则直接返回,如果未分出大小,则再比较位数,有子版本的为大; + return (diff != 0) ? diff : v1s.size() - v2s.size(); + } +} diff --git a/src/main/java/cn/hutool/core/comparator/package-info.java b/src/main/java/cn/hutool/core/comparator/package-info.java new file mode 100644 index 0000000..cc66cce --- /dev/null +++ b/src/main/java/cn/hutool/core/comparator/package-info.java @@ -0,0 +1,7 @@ +/** + * 各种比较器(Comparator)实现和封装 + * + * @author looly + * + */ +package cn.hutool.core.comparator; \ No newline at end of file diff --git a/src/main/java/cn/hutool/core/compress/Deflate.java b/src/main/java/cn/hutool/core/compress/Deflate.java new file mode 100644 index 0000000..4cb1856 --- /dev/null +++ b/src/main/java/cn/hutool/core/compress/Deflate.java @@ -0,0 +1,102 @@ +package cn.hutool.core.compress; + +import cn.hutool.core.io.IORuntimeException; +import cn.hutool.core.io.IoUtil; + +import java.io.Closeable; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.zip.Deflater; +import java.util.zip.DeflaterOutputStream; +import java.util.zip.Inflater; +import java.util.zip.InflaterOutputStream; + +/** + * Deflate算法
+ * Deflate是同时使用了LZ77算法与哈夫曼编码(Huffman Coding)的一个无损数据压缩算法。 + * + * @author looly + * @since 5.7.8 + */ +public class Deflate implements Closeable { + + private final InputStream source; + private OutputStream target; + private final boolean nowrap; + + /** + * 创建Deflate + * + * @param source 源流 + * @param target 目标流 + * @param nowrap {@code true}表示兼容Gzip压缩 + * @return this + */ + public static Deflate of(InputStream source, OutputStream target, boolean nowrap) { + return new Deflate(source, target, nowrap); + } + + /** + * 构造 + * + * @param source 源流 + * @param target 目标流 + * @param nowrap {@code true}表示兼容Gzip压缩 + */ + public Deflate(InputStream source, OutputStream target, boolean nowrap) { + this.source = source; + this.target = target; + this.nowrap = nowrap; + } + + /** + * 获取目标流 + * + * @return 目标流 + */ + public OutputStream getTarget() { + return this.target; + } + + /** + * 将普通数据流压缩 + * + * @param level 压缩级别,0~9 + * @return this + */ + public Deflate deflater(int level) { + target= (target instanceof DeflaterOutputStream) ? + (DeflaterOutputStream) target : new DeflaterOutputStream(target, new Deflater(level, nowrap)); + IoUtil.copy(source, target); + try { + ((DeflaterOutputStream)target).finish(); + } catch (IOException e) { + throw new IORuntimeException(e); + } + return this; + } + + /** + * 将压缩流解压到target中 + * + * @return this + */ + public Deflate inflater() { + target = (target instanceof InflaterOutputStream) ? + (InflaterOutputStream) target : new InflaterOutputStream(target, new Inflater(nowrap)); + IoUtil.copy(source, target); + try { + ((InflaterOutputStream)target).finish(); + } catch (IOException e) { + throw new IORuntimeException(e); + } + return this; + } + + @Override + public void close() { + IoUtil.close(this.target); + IoUtil.close(this.source); + } +} diff --git a/src/main/java/cn/hutool/core/compress/Gzip.java b/src/main/java/cn/hutool/core/compress/Gzip.java new file mode 100644 index 0000000..7d76d87 --- /dev/null +++ b/src/main/java/cn/hutool/core/compress/Gzip.java @@ -0,0 +1,94 @@ +package cn.hutool.core.compress; + +import cn.hutool.core.io.IORuntimeException; +import cn.hutool.core.io.IoUtil; + +import java.io.Closeable; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.zip.GZIPInputStream; +import java.util.zip.GZIPOutputStream; + +/** + * GZIP是用于Unix系统的文件压缩
+ * gzip的基础是DEFLATE + * + * @author looly + * @since 5.7.8 + */ +public class Gzip implements Closeable { + + private InputStream source; + private OutputStream target; + + /** + * 创建Gzip + * + * @param source 源流 + * @param target 目标流 + * @return Gzip + */ + public static Gzip of(InputStream source, OutputStream target) { + return new Gzip(source, target); + } + + /** + * 构造 + * + * @param source 源流 + * @param target 目标流 + */ + public Gzip(InputStream source, OutputStream target) { + this.source = source; + this.target = target; + } + + /** + * 获取目标流 + * + * @return 目标流 + */ + public OutputStream getTarget() { + return this.target; + } + + /** + * 将普通数据流压缩 + * + * @return Gzip + */ + public Gzip gzip() { + try { + target = (target instanceof GZIPOutputStream) ? + (GZIPOutputStream) target : new GZIPOutputStream(target); + IoUtil.copy(source, target); + ((GZIPOutputStream) target).finish(); + } catch (IOException e) { + throw new IORuntimeException(e); + } + return this; + } + + /** + * 将压缩流解压到target中 + * + * @return Gzip + */ + public Gzip unGzip() { + try { + source = (source instanceof GZIPInputStream) ? + (GZIPInputStream) source : new GZIPInputStream(source); + IoUtil.copy(source, target); + } catch (IOException e) { + throw new IORuntimeException(e); + } + return this; + } + + @Override + public void close() { + IoUtil.close(this.target); + IoUtil.close(this.source); + } +} diff --git a/src/main/java/cn/hutool/core/compress/ZipCopyVisitor.java b/src/main/java/cn/hutool/core/compress/ZipCopyVisitor.java new file mode 100644 index 0000000..02c3238 --- /dev/null +++ b/src/main/java/cn/hutool/core/compress/ZipCopyVisitor.java @@ -0,0 +1,88 @@ +package cn.hutool.core.compress; + +import cn.hutool.core.util.StrUtil; + +import java.io.IOException; +import java.nio.file.CopyOption; +import java.nio.file.DirectoryNotEmptyException; +import java.nio.file.FileAlreadyExistsException; +import java.nio.file.FileSystem; +import java.nio.file.FileVisitResult; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.attribute.BasicFileAttributes; + +/** + * Zip文件拷贝的FileVisitor实现,zip中追加文件,此类非线程安全
+ * 此类在遍历源目录并复制过程中会自动创建目标目录中不存在的上级目录。 + * + * @author looly + * @since 5.7.15 + */ +public class ZipCopyVisitor extends SimpleFileVisitor { + + /** + * 源Path,或基准路径,用于计算被拷贝文件的相对路径 + */ + private final Path source; + private final FileSystem fileSystem; + private final CopyOption[] copyOptions; + + /** + * 构造 + * + * @param source 源Path,或基准路径,用于计算被拷贝文件的相对路径 + * @param fileSystem 目标Zip文件 + * @param copyOptions 拷贝选项,如跳过已存在等 + */ + public ZipCopyVisitor(Path source, FileSystem fileSystem, CopyOption... copyOptions) { + this.source = source; + this.fileSystem = fileSystem; + this.copyOptions = copyOptions; + } + + @Override + public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException { + final Path targetDir = resolveTarget(dir); + if(StrUtil.isNotEmpty(targetDir.toString())){ + // 在目标的Zip文件中的相对位置创建目录 + try { + Files.copy(dir, targetDir, copyOptions); + } catch (final DirectoryNotEmptyException ignore) { + // 目录已经存在,则跳过 + } catch (FileAlreadyExistsException e) { + if (!Files.isDirectory(targetDir)) { + throw e; + } + // 目录非空情况下,跳过创建目录 + } + } + + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { + // 如果目标存在,无论目录还是文件都抛出FileAlreadyExistsException异常,此处不做特别处理 + Files.copy(file, resolveTarget(file), copyOptions); + + return FileVisitResult.CONTINUE; + } + + /** + * 根据源文件或目录路径,拼接生成目标的文件或目录路径
+ * 原理是首先截取源路径,得到相对路径,再和目标路径拼接 + * + *

+ * 如:源路径是 /opt/test/,需要拷贝的文件是 /opt/test/a/a.txt,得到相对路径 a/a.txt
+ * 目标路径是/home/,则得到最终目标路径是 /home/a/a.txt + *

+ * + * @param file 需要拷贝的文件或目录Path + * @return 目标Path + */ + private Path resolveTarget(Path file) { + return fileSystem.getPath(source.relativize(file).toString()); + } +} diff --git a/src/main/java/cn/hutool/core/compress/ZipReader.java b/src/main/java/cn/hutool/core/compress/ZipReader.java new file mode 100644 index 0000000..e9d5d9e --- /dev/null +++ b/src/main/java/cn/hutool/core/compress/ZipReader.java @@ -0,0 +1,251 @@ +package cn.hutool.core.compress; + +import cn.hutool.core.exceptions.UtilException; +import cn.hutool.core.io.FileUtil; +import cn.hutool.core.io.IORuntimeException; +import cn.hutool.core.io.IoUtil; +import cn.hutool.core.lang.Filter; +import cn.hutool.core.util.StrUtil; +import cn.hutool.core.util.ZipUtil; + +import java.io.Closeable; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.Charset; +import java.util.Enumeration; +import java.util.function.Consumer; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; +import java.util.zip.ZipInputStream; + +/** + * Zip文件或流读取器,一般用于Zip文件解压 + * + * @author looly + * @since 5.7.8 + */ +public class ZipReader implements Closeable { + + // size of uncompressed zip entry shouldn't be bigger of compressed in MAX_SIZE_DIFF times + private static final int MAX_SIZE_DIFF = 100; + + private ZipFile zipFile; + private ZipInputStream in; + + /** + * 创建ZipReader + * + * @param zipFile 生成的Zip文件 + * @param charset 编码 + * @return ZipReader + */ + public static ZipReader of(File zipFile, Charset charset) { + return new ZipReader(zipFile, charset); + } + + /** + * 创建ZipReader + * + * @param in Zip输入的流,一般为输入文件流 + * @param charset 编码 + * @return ZipReader + */ + public static ZipReader of(InputStream in, Charset charset) { + return new ZipReader(in, charset); + } + + /** + * 构造 + * + * @param zipFile 读取的的Zip文件 + * @param charset 编码 + */ + public ZipReader(File zipFile, Charset charset) { + this.zipFile = ZipUtil.toZipFile(zipFile, charset); + } + + /** + * 构造 + * + * @param zipFile 读取的的Zip文件 + */ + public ZipReader(ZipFile zipFile) { + this.zipFile = zipFile; + } + + /** + * 构造 + * + * @param in 读取的的Zip文件流 + * @param charset 编码 + */ + public ZipReader(InputStream in, Charset charset) { + this.in = new ZipInputStream(in, charset); + } + + /** + * 构造 + * + * @param zin 读取的的Zip文件流 + */ + public ZipReader(ZipInputStream zin) { + this.in = zin; + } + + /** + * 获取指定路径的文件流
+ * 如果是文件模式,则直接获取Entry对应的流,如果是流模式,则遍历entry后,找到对应流返回 + * + * @param path 路径 + * @return 文件流 + */ + public InputStream get(String path) { + if (null != this.zipFile) { + final ZipFile zipFile = this.zipFile; + final ZipEntry entry = zipFile.getEntry(path); + if (null != entry) { + return ZipUtil.getStream(zipFile, entry); + } + } else { + try { + this.in.reset(); + ZipEntry zipEntry; + while (null != (zipEntry = in.getNextEntry())) { + if (zipEntry.getName().equals(path)) { + return this.in; + } + } + } catch (IOException e) { + throw new IORuntimeException(e); + } + } + + return null; + } + + /** + * 解压到指定目录中 + * + * @param outFile 解压到的目录 + * @return 解压的目录 + * @throws IORuntimeException IO异常 + */ + public File readTo(File outFile) throws IORuntimeException { + return readTo(outFile, null); + } + + /** + * 解压到指定目录中 + * + * @param outFile 解压到的目录 + * @param entryFilter 过滤器,排除不需要的文件 + * @return 解压的目录 + * @throws IORuntimeException IO异常 + * @since 5.7.12 + */ + public File readTo(File outFile, Filter entryFilter) throws IORuntimeException { + read((zipEntry) -> { + if (null == entryFilter || entryFilter.accept(zipEntry)) { + //gitee issue #I4ZDQI + String path = zipEntry.getName(); + if (FileUtil.isWindows()) { + // Win系统下 + path = StrUtil.replace(path, "*", "_"); + } + // FileUtil.file会检查slip漏洞,漏洞说明见http://blog.nsfocus.net/zip-slip-2/ + final File outItemFile = FileUtil.file(outFile, path); + if (zipEntry.isDirectory()) { + // 目录 + //noinspection ResultOfMethodCallIgnored + outItemFile.mkdirs(); + } else { + InputStream in; + if (null != this.zipFile) { + in = ZipUtil.getStream(this.zipFile, zipEntry); + } else { + in = this.in; + } + // 文件 + FileUtil.writeFromStream(in, outItemFile, false); + } + } + }); + return outFile; + } + + /** + * 读取并处理Zip文件中的每一个{@link ZipEntry} + * + * @param consumer {@link ZipEntry}处理器 + * @return this + * @throws IORuntimeException IO异常 + */ + public ZipReader read(Consumer consumer) throws IORuntimeException { + if (null != this.zipFile) { + readFromZipFile(consumer); + } else { + readFromStream(consumer); + } + return this; + } + + @Override + public void close() throws IORuntimeException { + if (null != this.zipFile) { + IoUtil.close(this.zipFile); + } else { + IoUtil.close(this.in); + } + } + + /** + * 读取并处理Zip文件中的每一个{@link ZipEntry} + * + * @param consumer {@link ZipEntry}处理器 + */ + private void readFromZipFile(Consumer consumer) { + final Enumeration em = zipFile.entries(); + while (em.hasMoreElements()) { + consumer.accept(checkZipBomb(em.nextElement())); + } + } + + /** + * 读取并处理Zip流中的每一个{@link ZipEntry} + * + * @param consumer {@link ZipEntry}处理器 + * @throws IORuntimeException IO异常 + */ + private void readFromStream(Consumer consumer) throws IORuntimeException { + try { + ZipEntry zipEntry; + while (null != (zipEntry = in.getNextEntry())) { + consumer.accept(checkZipBomb(zipEntry)); + } + } catch (IOException e) { + throw new IORuntimeException(e); + } + } + + /** + * 检查Zip bomb漏洞 + * + * @param entry {@link ZipEntry} + * @return 检查后的{@link ZipEntry} + */ + private static ZipEntry checkZipBomb(ZipEntry entry) { + if (null == entry) { + return null; + } + final long compressedSize = entry.getCompressedSize(); + final long uncompressedSize = entry.getSize(); + if (compressedSize < 0 || uncompressedSize < 0 || + // 默认压缩比例是100倍,一旦发现压缩率超过这个阈值,被认为是Zip bomb + compressedSize * MAX_SIZE_DIFF < uncompressedSize) { + throw new UtilException("Zip bomb attack detected, invalid sizes: compressed {}, uncompressed {}, name {}", + compressedSize, uncompressedSize, entry.getName()); + } + return entry; + } +} diff --git a/src/main/java/cn/hutool/core/compress/ZipWriter.java b/src/main/java/cn/hutool/core/compress/ZipWriter.java new file mode 100644 index 0000000..f2266f8 --- /dev/null +++ b/src/main/java/cn/hutool/core/compress/ZipWriter.java @@ -0,0 +1,287 @@ +package cn.hutool.core.compress; + +import cn.hutool.core.io.FileUtil; +import cn.hutool.core.io.IORuntimeException; +import cn.hutool.core.io.IoUtil; +import cn.hutool.core.io.resource.Resource; +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.core.util.ZipUtil; + +import java.io.Closeable; +import java.io.File; +import java.io.FileFilter; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.charset.Charset; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; + +/** + * Zip生成封装 + * + * @author looly + * @since 5.7.8 + */ +public class ZipWriter implements Closeable { + + /** + * 创建ZipWriter + * + * @param zipFile 生成的Zip文件 + * @param charset 编码 + * @return ZipWriter + */ + public static ZipWriter of(File zipFile, Charset charset) { + return new ZipWriter(zipFile, charset); + } + + /** + * 创建ZipWriter + * + * @param out Zip输出的流,一般为输出文件流 + * @param charset 编码 + * @return ZipWriter + */ + public static ZipWriter of(OutputStream out, Charset charset) { + return new ZipWriter(out, charset); + } + + private final ZipOutputStream out; + + /** + * 构造 + * + * @param zipFile 生成的Zip文件 + * @param charset 编码 + */ + public ZipWriter(File zipFile, Charset charset) { + this.out = getZipOutputStream(zipFile, charset); + } + + /** + * 构造 + * + * @param out {@link ZipOutputStream} + * @param charset 编码 + */ + public ZipWriter(OutputStream out, Charset charset) { + this.out = ZipUtil.getZipOutputStream(out, charset); + } + + /** + * 构造 + * + * @param out {@link ZipOutputStream} + */ + public ZipWriter(ZipOutputStream out) { + this.out = out; + } + + /** + * 设置压缩级别,可选1~9,-1表示默认 + * + * @param level 压缩级别 + * @return this + */ + public ZipWriter setLevel(int level) { + this.out.setLevel(level); + return this; + } + + /** + * 设置注释 + * + * @param comment 注释 + * @return this + */ + public ZipWriter setComment(String comment) { + this.out.setComment(comment); + return this; + } + + /** + * 获取原始的{@link ZipOutputStream} + * + * @return {@link ZipOutputStream} + */ + public ZipOutputStream getOut() { + return this.out; + } + + /** + * 对文件或文件目录进行压缩 + * + * @param withSrcDir 是否包含被打包目录,只针对压缩目录有效。若为false,则只压缩目录下的文件或目录,为true则将本目录也压缩 + * @param filter 文件过滤器,通过实现此接口,自定义要过滤的文件(过滤掉哪些文件或文件夹不加入压缩),{@code null}表示不过滤 + * @param files 要压缩的源文件或目录。如果压缩一个文件,则为该文件的全路径;如果压缩一个目录,则为该目录的顶层目录路径 + * @return this + * @throws IORuntimeException IO异常 + * @since 5.1.1 + */ + public ZipWriter add(boolean withSrcDir, FileFilter filter, File... files) throws IORuntimeException { + for (File file : files) { + // 如果只是压缩一个文件,则需要截取该文件的父目录 + String srcRootDir; + try { + srcRootDir = file.getCanonicalPath(); + if ((!file.isDirectory()) || withSrcDir) { + // 若是文件,则将父目录完整路径都截取掉;若设置包含目录,则将上级目录全部截取掉,保留本目录名 + srcRootDir = file.getCanonicalFile().getParentFile().getCanonicalPath(); + } + } catch (IOException e) { + throw new IORuntimeException(e); + } + + _add(file, srcRootDir, filter); + } + return this; + } + + /** + * 添加资源到压缩包,添加后关闭资源流 + * + * @param resources 需要压缩的资源,资源的路径为{@link Resource#getName()} + * @return this + * @throws IORuntimeException IO异常 + */ + public ZipWriter add(Resource... resources) throws IORuntimeException { + for (Resource resource : resources) { + if (null != resource) { + add(resource.getName(), resource.getStream()); + } + } + return this; + } + + /** + * 添加文件流到压缩包,添加后关闭输入文件流
+ * 如果输入流为{@code null},则只创建空目录 + * + * @param path 压缩的路径, {@code null}和""表示根目录下 + * @param in 需要压缩的输入流,使用完后自动关闭,{@code null}表示加入空目录 + * @return this + * @throws IORuntimeException IO异常 + */ + public ZipWriter add(String path, InputStream in) throws IORuntimeException { + path = StrUtil.nullToEmpty(path); + if (null == in) { + // 空目录需要检查路径规范性,目录以"/"结尾 + path = StrUtil.addSuffixIfNot(path, StrUtil.SLASH); + if (StrUtil.isBlank(path)) { + return this; + } + } + + return putEntry(path, in); + } + + /** + * 对流中的数据加入到压缩文件
+ * 路径列表和流列表长度必须一致 + * + * @param paths 流数据在压缩文件中的路径或文件名 + * @param ins 要压缩的源,添加完成后自动关闭流 + * @return 压缩文件 + * @throws IORuntimeException IO异常 + * @since 5.8.0 + */ + public ZipWriter add(String[] paths, InputStream[] ins) throws IORuntimeException { + if (ArrayUtil.isEmpty(paths) || ArrayUtil.isEmpty(ins)) { + throw new IllegalArgumentException("Paths or ins is empty !"); + } + if (paths.length != ins.length) { + throw new IllegalArgumentException("Paths length is not equals to ins length !"); + } + + for (int i = 0; i < paths.length; i++) { + add(paths[i], ins[i]); + } + + return this; + } + + @Override + public void close() throws IORuntimeException { + try { + out.finish(); + } catch (IOException e) { + throw new IORuntimeException(e); + } finally { + IoUtil.close(this.out); + } + } + + /** + * 获得 {@link ZipOutputStream} + * + * @param zipFile 压缩文件 + * @param charset 编码 + * @return {@link ZipOutputStream} + */ + private static ZipOutputStream getZipOutputStream(File zipFile, Charset charset) { + return ZipUtil.getZipOutputStream(FileUtil.getOutputStream(zipFile), charset); + } + + /** + * 递归压缩文件夹或压缩文件
+ * srcRootDir决定了路径截取的位置,例如:
+ * file的路径为d:/a/b/c/d.txt,srcRootDir为d:/a/b,则压缩后的文件与目录为结构为c/d.txt + * + * @param srcRootDir 被压缩的文件夹根目录 + * @param file 当前递归压缩的文件或目录对象 + * @param filter 文件过滤器,通过实现此接口,自定义要过滤的文件(过滤掉哪些文件或文件夹不加入压缩),{@code null}表示不过滤 + * @throws IORuntimeException IO异常 + */ + private ZipWriter _add(File file, String srcRootDir, FileFilter filter) throws IORuntimeException { + if (null == file || (null != filter && !filter.accept(file))) { + return this; + } + + // 获取文件相对于压缩文件夹根目录的子路径 + final String subPath = FileUtil.subPath(srcRootDir, file); + if (file.isDirectory()) { + // 如果是目录,则压缩压缩目录中的文件或子目录 + final File[] files = file.listFiles(); + if (ArrayUtil.isEmpty(files)) { + // 加入目录,只有空目录时才加入目录,非空时会在创建文件时自动添加父级目录 + add(subPath, null); + } else { + // 压缩目录下的子文件或目录 + for (File childFile : files) { + _add(childFile, srcRootDir, filter); + } + } + } else { + // 如果是文件或其它符号,则直接压缩该文件 + putEntry(subPath, FileUtil.getInputStream(file)); + } + return this; + } + + /** + * 添加文件流到压缩包,添加后关闭输入文件流
+ * 如果输入流为{@code null},则只创建空目录 + * + * @param path 压缩的路径, {@code null}和""表示根目录下 + * @param in 需要压缩的输入流,使用完后自动关闭,{@code null}表示加入空目录 + * @throws IORuntimeException IO异常 + */ + private ZipWriter putEntry(String path, InputStream in) throws IORuntimeException { + try { + out.putNextEntry(new ZipEntry(path)); + if (null != in) { + IoUtil.copy(in, out); + } + out.closeEntry(); + } catch (IOException e) { + throw new IORuntimeException(e); + } finally { + IoUtil.close(in); + } + + IoUtil.flush(this.out); + return this; + } +} diff --git a/src/main/java/cn/hutool/core/compress/package-info.java b/src/main/java/cn/hutool/core/compress/package-info.java new file mode 100644 index 0000000..d26b8a1 --- /dev/null +++ b/src/main/java/cn/hutool/core/compress/package-info.java @@ -0,0 +1,7 @@ +/** + * 压缩解压封装 + * + * @author looly + * @since 5.7.8 + */ +package cn.hutool.core.compress; diff --git a/src/main/java/cn/hutool/core/convert/AbstractConverter.java b/src/main/java/cn/hutool/core/convert/AbstractConverter.java new file mode 100644 index 0000000..22b2216 --- /dev/null +++ b/src/main/java/cn/hutool/core/convert/AbstractConverter.java @@ -0,0 +1,117 @@ +package cn.hutool.core.convert; + +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.CharUtil; +import cn.hutool.core.util.ClassUtil; +import cn.hutool.core.util.StrUtil; + +import java.io.Serializable; +import java.util.Map; + +/** + * 抽象转换器,提供通用的转换逻辑,同时通过convertInternal实现对应类型的专属逻辑
+ * 转换器不会抛出转换异常,转换失败时会返回{@code null} + * + * @author Looly + * + */ +public abstract class AbstractConverter implements Converter, Serializable { + private static final long serialVersionUID = 1L; + + /** + * 不抛异常转换
+ * 当转换失败时返回默认值 + * + * @param value 被转换的值 + * @param defaultValue 默认值 + * @return 转换后的值 + * @since 4.5.7 + */ + public T convertQuietly(Object value, T defaultValue) { + try { + return convert(value, defaultValue); + } catch (Exception e) { + return defaultValue; + } + } + + @Override + @SuppressWarnings("unchecked") + public T convert(Object value, T defaultValue) { + Class targetType = getTargetType(); + if (null == targetType && null == defaultValue) { + throw new NullPointerException(StrUtil.format("[type] and [defaultValue] are both null for Converter [{}], we can not know what type to convert !", this.getClass().getName())); + } + if (null == targetType) { + // 目标类型不确定时使用默认值的类型 + targetType = (Class) defaultValue.getClass(); + } + if (null == value) { + return defaultValue; + } + + if (null == defaultValue || targetType.isInstance(defaultValue)) { + if (targetType.isInstance(value) && !Map.class.isAssignableFrom(targetType)) { + // 除Map外,已经是目标类型,不需要转换(Map类型涉及参数类型,需要单独转换) + return targetType.cast(value); + } + final T result = convertInternal(value); + return ((null == result) ? defaultValue : result); + } else { + throw new IllegalArgumentException( + StrUtil.format("Default value [{}]({}) is not the instance of [{}]", defaultValue, defaultValue.getClass(), targetType)); + } + } + + /** + * 内部转换器,被 {@link AbstractConverter#convert(Object, Object)} 调用,实现基本转换逻辑
+ * 内部转换器转换后如果转换失败可以做如下操作,处理结果都为返回默认值: + * + *
+	 * 1、返回{@code null}
+	 * 2、抛出一个{@link RuntimeException}异常
+	 * 
+ * + * @param value 值 + * @return 转换后的类型 + */ + protected abstract T convertInternal(Object value); + + /** + * 值转为String,用于内部转换中需要使用String中转的情况
+ * 转换规则为: + * + *
+	 * 1、字符串类型将被强转
+	 * 2、数组将被转换为逗号分隔的字符串
+	 * 3、其它类型将调用默认的toString()方法
+	 * 
+ * + * @param value 值 + * @return String + */ + protected String convertToStr(Object value) { + if (null == value) { + return null; + } + if (value instanceof CharSequence) { + return value.toString(); + } else if (ArrayUtil.isArray(value)) { + return ArrayUtil.toString(value); + } else if(CharUtil.isChar(value)) { + //对于ASCII字符使用缓存加速转换,减少空间创建 + return CharUtil.toString((char)value); + } + return value.toString(); + } + + /** + * 获得此类实现类的泛型类型 + * + * @return 此类的泛型类型,可能为{@code null} + */ + @SuppressWarnings("unchecked") + public Class getTargetType() { + return (Class) ClassUtil.getTypeArgument(getClass()); + } +} diff --git a/src/main/java/cn/hutool/core/convert/BasicType.java b/src/main/java/cn/hutool/core/convert/BasicType.java new file mode 100644 index 0000000..9e33940 --- /dev/null +++ b/src/main/java/cn/hutool/core/convert/BasicType.java @@ -0,0 +1,60 @@ +package cn.hutool.core.convert; + +import cn.hutool.core.map.SafeConcurrentHashMap; + +import java.util.Map; + +/** + * 基本变量类型的枚举
+ * 基本类型枚举包括原始类型和包装类型 + * @author xiaoleilu + */ +public enum BasicType { + BYTE, SHORT, INT, INTEGER, LONG, DOUBLE, FLOAT, BOOLEAN, CHAR, CHARACTER, STRING; + + /** 包装类型为Key,原始类型为Value,例如: Integer.class =》 int.class. */ + public static final Map, Class> WRAPPER_PRIMITIVE_MAP = new SafeConcurrentHashMap<>(8); + /** 原始类型为Key,包装类型为Value,例如: int.class =》 Integer.class. */ + public static final Map, Class> PRIMITIVE_WRAPPER_MAP = new SafeConcurrentHashMap<>(8); + + static { + WRAPPER_PRIMITIVE_MAP.put(Boolean.class, boolean.class); + WRAPPER_PRIMITIVE_MAP.put(Byte.class, byte.class); + WRAPPER_PRIMITIVE_MAP.put(Character.class, char.class); + WRAPPER_PRIMITIVE_MAP.put(Double.class, double.class); + WRAPPER_PRIMITIVE_MAP.put(Float.class, float.class); + WRAPPER_PRIMITIVE_MAP.put(Integer.class, int.class); + WRAPPER_PRIMITIVE_MAP.put(Long.class, long.class); + WRAPPER_PRIMITIVE_MAP.put(Short.class, short.class); + + for (Map.Entry, Class> entry : WRAPPER_PRIMITIVE_MAP.entrySet()) { + PRIMITIVE_WRAPPER_MAP.put(entry.getValue(), entry.getKey()); + } + } + + /** + * 原始类转为包装类,非原始类返回原类 + * @param clazz 原始类 + * @return 包装类 + */ + public static Class wrap(Class clazz){ + if(null == clazz || !clazz.isPrimitive()){ + return clazz; + } + Class result = PRIMITIVE_WRAPPER_MAP.get(clazz); + return (null == result) ? clazz : result; + } + + /** + * 包装类转为原始类,非包装类返回原类 + * @param clazz 包装类 + * @return 原始类 + */ + public static Class unWrap(Class clazz){ + if(null == clazz || clazz.isPrimitive()){ + return clazz; + } + Class result = WRAPPER_PRIMITIVE_MAP.get(clazz); + return (null == result) ? clazz : result; + } +} diff --git a/src/main/java/cn/hutool/core/convert/CastUtil.java b/src/main/java/cn/hutool/core/convert/CastUtil.java new file mode 100644 index 0000000..5279bb7 --- /dev/null +++ b/src/main/java/cn/hutool/core/convert/CastUtil.java @@ -0,0 +1,118 @@ +package cn.hutool.core.convert; + +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * 转换工具类,提供集合、Map等向上向下转换工具 + * + * @author looly + * @since 5.8.1 + */ +public class CastUtil { + /** + * 泛型集合向上转型。例如将Collection<Integer>转换为Collection<Number> + * + * @param collection 集合 + * @param 元素类型 + * @return 转换后的集合 + * @since 5.8.1 + */ + @SuppressWarnings("unchecked") + public static Collection castUp(Collection collection) { + return (Collection) collection; + } + + /** + * 泛型集合向下转型。例如将Collection<Number>转换为Collection<Integer> + * + * @param collection 集合 + * @param 元素类型 + * @return 转换后的集合 + * @since 5.8.1 + */ + @SuppressWarnings("unchecked") + public static Collection castDown(Collection collection) { + return (Collection) collection; + } + + /** + * 泛型集合向上转型。例如将Set<Integer>转换为Set<Number> + * + * @param set 集合 + * @param 泛型 + * @return 泛化集合 + * @since 5.8.1 + */ + @SuppressWarnings("unchecked") + public static Set castUp(Set set) { + return (Set) set; + } + + /** + * 泛型集合向下转型。例如将Set<Number>转换为Set<Integer> + * + * @param set 集合 + * @param 泛型子类 + * @return 泛化集合 + * @since 5.8.1 + */ + @SuppressWarnings("unchecked") + public static Set castDown(Set set) { + return (Set) set; + } + + /** + * 泛型接口向上转型。例如将List<Integer>转换为List<Number> + * + * @param list 集合 + * @param 泛型的父类 + * @return 泛化集合 + */ + @SuppressWarnings("unchecked") + public static List castUp(List list) { + return (List) list; + } + + /** + * 泛型集合向下转型。例如将List<Number>转换为List<Integer> + * + * @param list 集合 + * @param 泛型的子类 + * @return 泛化集合 + */ + @SuppressWarnings("unchecked") + public static List castDown(List list) { + return (List) list; + } + + /** + * 泛型集合向下转型。例如将Map<Integer, Integer>转换为Map<Number,Number> + * + * @param map 集合 + * @param 泛型父类 + * @param 泛型父类 + * @return 泛化集合 + * @since 5.8.1 + */ + @SuppressWarnings("unchecked") + public static Map castUp(Map map) { + return (Map) map; + } + + /** + * 泛型集合向下转型。例如将Map<Number,Number>转换为Map<Integer, Integer> + * + * @param map 集合 + * @param 泛型子类 + * @param 泛型子类 + * @return 泛化集合 + * @since 5.8.1 + */ + @SuppressWarnings("unchecked") + public static Map castDown(Map map) { + return (Map) map; + } +} diff --git a/src/main/java/cn/hutool/core/convert/Convert.java b/src/main/java/cn/hutool/core/convert/Convert.java new file mode 100644 index 0000000..b64d555 --- /dev/null +++ b/src/main/java/cn/hutool/core/convert/Convert.java @@ -0,0 +1,1162 @@ +package cn.hutool.core.convert; + +import cn.hutool.core.convert.impl.CollectionConverter; +import cn.hutool.core.convert.impl.EnumConverter; +import cn.hutool.core.convert.impl.MapConverter; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.lang.TypeReference; +import cn.hutool.core.text.UnicodeUtil; +import cn.hutool.core.util.ByteUtil; +import cn.hutool.core.util.CharUtil; +import cn.hutool.core.util.CharsetUtil; +import cn.hutool.core.util.ClassUtil; +import cn.hutool.core.util.HexUtil; +import cn.hutool.core.util.StrUtil; + +import java.lang.reflect.Type; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.nio.charset.Charset; +import java.time.Instant; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.TimeUnit; + +/** + * 类型转换器 + * + * @author xiaoleilu + */ +public class Convert { + + /** + * 转换为字符串
+ * 如果给定的值为null,或者转换失败,返回默认值
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @param defaultValue 转换错误时的默认值 + * @return 结果 + */ + public static String toStr(Object value, String defaultValue) { + return convertQuietly(String.class, value, defaultValue); + } + + /** + * 转换为字符串
+ * 如果给定的值为{@code null},或者转换失败,返回默认值{@code null}
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @return 结果 + */ + public static String toStr(Object value) { + return toStr(value, null); + } + + /** + * 转换为String数组 + * + * @param value 被转换的值 + * @return String数组 + * @since 3.2.0 + */ + public static String[] toStrArray(Object value) { + return convert(String[].class, value); + } + + /** + * 转换为字符
+ * 如果给定的值为null,或者转换失败,返回默认值
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @param defaultValue 转换错误时的默认值 + * @return 结果 + */ + public static Character toChar(Object value, Character defaultValue) { + return convertQuietly(Character.class, value, defaultValue); + } + + /** + * 转换为字符
+ * 如果给定的值为{@code null},或者转换失败,返回默认值{@code null}
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @return 结果 + */ + public static Character toChar(Object value) { + return toChar(value, null); + } + + /** + * 转换为Character数组 + * + * @param value 被转换的值 + * @return Character数组 + * @since 3.2.0 + */ + public static Character[] toCharArray(Object value) { + return convert(Character[].class, value); + } + + /** + * 转换为byte
+ * 如果给定的值为{@code null},或者转换失败,返回默认值
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @param defaultValue 转换错误时的默认值 + * @return 结果 + */ + public static Byte toByte(Object value, Byte defaultValue) { + return convertQuietly(Byte.class, value, defaultValue); + } + + /** + * 转换为byte
+ * 如果给定的值为{@code null},或者转换失败,返回默认值{@code null}
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @return 结果 + */ + public static Byte toByte(Object value) { + return toByte(value, null); + } + + /** + * 转换为Byte数组 + * + * @param value 被转换的值 + * @return Byte数组 + * @since 3.2.0 + */ + public static Byte[] toByteArray(Object value) { + return convert(Byte[].class, value); + } + + /** + * 转换为Byte数组 + * + * @param value 被转换的值 + * @return Byte数组 + * @since 5.1.1 + */ + public static byte[] toPrimitiveByteArray(Object value) { + return convert(byte[].class, value); + } + + /** + * 转换为Short
+ * 如果给定的值为{@code null},或者转换失败,返回默认值
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @param defaultValue 转换错误时的默认值 + * @return 结果 + */ + public static Short toShort(Object value, Short defaultValue) { + return convertQuietly(Short.class, value, defaultValue); + } + + /** + * 转换为Short
+ * 如果给定的值为{@code null},或者转换失败,返回默认值{@code null}
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @return 结果 + */ + public static Short toShort(Object value) { + return toShort(value, null); + } + + /** + * 转换为Short数组 + * + * @param value 被转换的值 + * @return Short数组 + * @since 3.2.0 + */ + public static Short[] toShortArray(Object value) { + return convert(Short[].class, value); + } + + /** + * 转换为Number
+ * 如果给定的值为空,或者转换失败,返回默认值
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @param defaultValue 转换错误时的默认值 + * @return 结果 + */ + public static Number toNumber(Object value, Number defaultValue) { + return convertQuietly(Number.class, value, defaultValue); + } + + /** + * 转换为Number
+ * 如果给定的值为空,或者转换失败,返回默认值{@code null}
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @return 结果 + */ + public static Number toNumber(Object value) { + return toNumber(value, null); + } + + /** + * 转换为Number数组 + * + * @param value 被转换的值 + * @return Number数组 + * @since 3.2.0 + */ + public static Number[] toNumberArray(Object value) { + return convert(Number[].class, value); + } + + /** + * 转换为int
+ * 如果给定的值为空,或者转换失败,返回默认值
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @param defaultValue 转换错误时的默认值 + * @return 结果 + */ + public static Integer toInt(Object value, Integer defaultValue) { + return convertQuietly(Integer.class, value, defaultValue); + } + + /** + * 转换为int
+ * 如果给定的值为{@code null},或者转换失败,返回默认值{@code null}
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @return 结果 + */ + public static Integer toInt(Object value) { + return toInt(value, null); + } + + /** + * 转换为Integer数组
+ * + * @param value 被转换的值 + * @return 结果 + */ + public static Integer[] toIntArray(Object value) { + return convert(Integer[].class, value); + } + + /** + * 转换为long
+ * 如果给定的值为空,或者转换失败,返回默认值
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @param defaultValue 转换错误时的默认值 + * @return 结果 + */ + public static Long toLong(Object value, Long defaultValue) { + return convertQuietly(Long.class, value, defaultValue); + } + + /** + * 转换为long
+ * 如果给定的值为{@code null},或者转换失败,返回默认值{@code null}
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @return 结果 + */ + public static Long toLong(Object value) { + return toLong(value, null); + } + + /** + * 转换为Long数组
+ * + * @param value 被转换的值 + * @return 结果 + */ + public static Long[] toLongArray(Object value) { + return convert(Long[].class, value); + } + + /** + * 转换为double
+ * 如果给定的值为空,或者转换失败,返回默认值
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @param defaultValue 转换错误时的默认值 + * @return 结果 + */ + public static Double toDouble(Object value, Double defaultValue) { + return convertQuietly(Double.class, value, defaultValue); + } + + /** + * 转换为double
+ * 如果给定的值为空,或者转换失败,返回默认值{@code null}
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @return 结果 + */ + public static Double toDouble(Object value) { + return toDouble(value, null); + } + + /** + * 转换为Double数组
+ * + * @param value 被转换的值 + * @return 结果 + */ + public static Double[] toDoubleArray(Object value) { + return convert(Double[].class, value); + } + + /** + * 转换为Float
+ * 如果给定的值为空,或者转换失败,返回默认值
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @param defaultValue 转换错误时的默认值 + * @return 结果 + */ + public static Float toFloat(Object value, Float defaultValue) { + return convertQuietly(Float.class, value, defaultValue); + } + + /** + * 转换为Float
+ * 如果给定的值为空,或者转换失败,返回默认值{@code null}
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @return 结果 + */ + public static Float toFloat(Object value) { + return toFloat(value, null); + } + + /** + * 转换为Float数组
+ * + * @param value 被转换的值 + * @return 结果 + */ + public static Float[] toFloatArray(Object value) { + return convert(Float[].class, value); + } + + /** + * 转换为boolean
+ * String支持的值为:true、false、yes、ok、no,1,0 如果给定的值为空,或者转换失败,返回默认值
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @param defaultValue 转换错误时的默认值 + * @return 结果 + */ + public static Boolean toBool(Object value, Boolean defaultValue) { + return convertQuietly(Boolean.class, value, defaultValue); + } + + /** + * 转换为boolean
+ * 如果给定的值为空,或者转换失败,返回默认值{@code null}
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @return 结果 + */ + public static Boolean toBool(Object value) { + return toBool(value, null); + } + + /** + * 转换为Boolean数组
+ * + * @param value 被转换的值 + * @return 结果 + */ + public static Boolean[] toBooleanArray(Object value) { + return convert(Boolean[].class, value); + } + + /** + * 转换为BigInteger
+ * 如果给定的值为空,或者转换失败,返回默认值
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @param defaultValue 转换错误时的默认值 + * @return 结果 + */ + public static BigInteger toBigInteger(Object value, BigInteger defaultValue) { + return convertQuietly(BigInteger.class, value, defaultValue); + } + + /** + * 转换为BigInteger
+ * 如果给定的值为空,或者转换失败,返回默认值{@code null}
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @return 结果 + */ + public static BigInteger toBigInteger(Object value) { + return toBigInteger(value, null); + } + + /** + * 转换为BigDecimal
+ * 如果给定的值为空,或者转换失败,返回默认值
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @param defaultValue 转换错误时的默认值 + * @return 结果 + */ + public static BigDecimal toBigDecimal(Object value, BigDecimal defaultValue) { + return convertQuietly(BigDecimal.class, value, defaultValue); + } + + /** + * 转换为BigDecimal
+ * 如果给定的值为空,或者转换失败,返回null
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @return 结果 + */ + public static BigDecimal toBigDecimal(Object value) { + return toBigDecimal(value, null); + } + + /** + * 转换为Date
+ * 如果给定的值为空,或者转换失败,返回默认值
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @param defaultValue 转换错误时的默认值 + * @return 结果 + * @since 4.1.6 + */ + public static Date toDate(Object value, Date defaultValue) { + return convertQuietly(Date.class, value, defaultValue); + } + + /** + * LocalDateTime
+ * 如果给定的值为空,或者转换失败,返回默认值
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @param defaultValue 转换错误时的默认值 + * @return 结果 + * @since 5.0.7 + */ + public static LocalDateTime toLocalDateTime(Object value, LocalDateTime defaultValue) { + return convertQuietly(LocalDateTime.class, value, defaultValue); + } + + /** + * 转换为LocalDateTime
+ * 如果给定的值为空,或者转换失败,返回{@code null}
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @return 结果 + */ + public static LocalDateTime toLocalDateTime(Object value) { + return toLocalDateTime(value, null); + } + + /** + * Instant
+ * 如果给定的值为空,或者转换失败,返回默认值
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @param defaultValue 转换错误时的默认值 + * @return 结果 + * @since 5.0.7 + */ + public static Date toInstant(Object value, Date defaultValue) { + return convertQuietly(Instant.class, value, defaultValue); + } + + /** + * 转换为Date
+ * 如果给定的值为空,或者转换失败,返回{@code null}
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @return 结果 + * @since 4.1.6 + */ + public static Date toDate(Object value) { + return toDate(value, null); + } + + /** + * 转换为Enum对象
+ * 如果给定的值为空,或者转换失败,返回默认值
+ * + * @param 枚举类型 + * @param clazz Enum的Class + * @param value 值 + * @param defaultValue 默认值 + * @return Enum + */ + @SuppressWarnings("unchecked") + public static > E toEnum(Class clazz, Object value, E defaultValue) { + return (E) (new EnumConverter(clazz)).convertQuietly(value, defaultValue); + } + + /** + * 转换为Enum对象
+ * 如果给定的值为空,或者转换失败,返回默认值{@code null}
+ * + * @param 枚举类型 + * @param clazz Enum的Class + * @param value 值 + * @return Enum + */ + public static > E toEnum(Class clazz, Object value) { + return toEnum(clazz, value, null); + } + + /** + * 转换为集合类 + * + * @param collectionType 集合类型 + * @param elementType 集合中元素类型 + * @param value 被转换的值 + * @return {@link Collection} + * @since 3.0.8 + */ + public static Collection toCollection(Class collectionType, Class elementType, Object value) { + return new CollectionConverter(collectionType, elementType).convert(value, null); + } + + /** + * 转换为ArrayList,元素类型默认Object + * + * @param value 被转换的值 + * @return {@link List} + * @since 4.1.11 + */ + public static List toList(Object value) { + return convert(List.class, value); + } + + /** + * 转换为ArrayList + * + * @param 元素类型 + * @param elementType 集合中元素类型 + * @param value 被转换的值 + * @return {@link ArrayList} + * @since 4.1.20 + */ + @SuppressWarnings("unchecked") + public static List toList(Class elementType, Object value) { + return (List) toCollection(ArrayList.class, elementType, value); + } + + /** + * 转换为HashSet + * + * @param 元素类型 + * @param elementType 集合中元素类型 + * @param value 被转换的值 + * @return {@link HashSet} + * @since 5.7.3 + */ + @SuppressWarnings("unchecked") + public static Set toSet(Class elementType, Object value) { + return (Set) toCollection(HashSet.class, elementType, value); + } + + /** + * 转换为Map,若value原本就是Map,则转为原始类型,若不是则默认转为HashMap + * + * @param 键类型 + * @param 值类型 + * @param keyType 键类型 + * @param valueType 值类型 + * @param value 被转换的值 + * @return {@link Map} + * @since 4.6.8 + */ + @SuppressWarnings("unchecked") + public static Map toMap(Class keyType, Class valueType, Object value) { + if (value instanceof Map) { + return toMap((Class>) value.getClass(), keyType, valueType, value); + } else { + return toMap(HashMap.class, keyType, valueType, value); + } + } + + /** + * 转换为Map + * + * @param mapType 转后的具体Map类型 + * @param 键类型 + * @param 值类型 + * @param keyType 键类型 + * @param valueType 值类型 + * @param value 被转换的值 + * @return {@link Map} + */ + @SuppressWarnings({"unchecked", "rawtypes"}) + public static Map toMap(Class mapType, Class keyType, Class valueType, Object value) { + return (Map) new MapConverter(mapType, keyType, valueType).convert(value, null); + } + + /** + * 转换值为指定类型,类型采用字符串表示 + * + * @param 目标类型 + * @param className 类的字符串表示 + * @param value 值 + * @return 转换后的值 + * @throws ConvertException 转换器不存在 + * @since 4.0.7 + */ + public static T convertByClassName(String className, Object value) throws ConvertException { + return convert(ClassUtil.loadClass(className), value); + } + + /** + * 转换值为指定类型 + * + * @param 目标类型 + * @param type 类型 + * @param value 值 + * @return 转换后的值 + * @throws ConvertException 转换器不存在 + * @since 4.0.0 + */ + public static T convert(Class type, Object value) throws ConvertException { + return convert((Type) type, value); + } + + /** + * 转换值为指定类型 + * + * @param 目标类型 + * @param reference 类型参考,用于持有转换后的泛型类型 + * @param value 值 + * @return 转换后的值 + * @throws ConvertException 转换器不存在 + */ + public static T convert(TypeReference reference, Object value) throws ConvertException { + return convert(reference.getType(), value, null); + } + + /** + * 转换值为指定类型 + * + * @param 目标类型 + * @param type 类型 + * @param value 值 + * @return 转换后的值 + * @throws ConvertException 转换器不存在 + */ + public static T convert(Type type, Object value) throws ConvertException { + return convert(type, value, null); + } + + /** + * 转换值为指定类型 + * + * @param 目标类型 + * @param type 类型 + * @param value 值 + * @param defaultValue 默认值 + * @return 转换后的值 + * @throws ConvertException 转换器不存在 + * @since 4.0.0 + */ + public static T convert(Class type, Object value, T defaultValue) throws ConvertException { + return convert((Type) type, value, defaultValue); + } + + /** + * 转换值为指定类型 + * + * @param 目标类型 + * @param type 类型 + * @param value 值 + * @param defaultValue 默认值 + * @return 转换后的值 + * @throws ConvertException 转换器不存在 + */ + public static T convert(Type type, Object value, T defaultValue) throws ConvertException { + return convertWithCheck(type, value, defaultValue, false); + } + + /** + * 转换值为指定类型,不抛异常转换
+ * 当转换失败时返回{@code null} + * + * @param 目标类型 + * @param type 目标类型 + * @param value 值 + * @return 转换后的值,转换失败返回null + * @since 4.5.10 + */ + public static T convertQuietly(Type type, Object value) { + return convertQuietly(type, value, null); + } + + /** + * 转换值为指定类型,不抛异常转换
+ * 当转换失败时返回默认值 + * + * @param 目标类型 + * @param type 目标类型 + * @param value 值 + * @param defaultValue 默认值 + * @return 转换后的值 + * @since 4.5.10 + */ + public static T convertQuietly(Type type, Object value, T defaultValue) { + return convertWithCheck(type, value, defaultValue, true); + } + + /** + * 转换值为指定类型,可选是否不抛异常转换
+ * 当转换失败时返回默认值 + * + * @param 目标类型 + * @param type 目标类型 + * @param value 值 + * @param defaultValue 默认值 + * @param quietly 是否静默转换,true不抛异常 + * @return 转换后的值 + * @since 5.3.2 + */ + public static T convertWithCheck(Type type, Object value, T defaultValue, boolean quietly) { + final ConverterRegistry registry = ConverterRegistry.getInstance(); + try { + return registry.convert(type, value, defaultValue); + } catch (Exception e) { + if (quietly) { + return defaultValue; + } + throw e; + } + } + + // ----------------------------------------------------------------------- 全角半角转换 + + /** + * 半角转全角,{@code null}返回{@code null} + * + * @param input String. + * @return 全角字符串,{@code null}返回{@code null} + */ + public static String toSBC(String input) { + return toSBC(input, null); + } + + /** + * 半角转全角,{@code null}返回{@code null} + * + * @param input String + * @param notConvertSet 不替换的字符集合 + * @return 全角字符串,{@code null}返回{@code null} + */ + public static String toSBC(String input, Set notConvertSet) { + if (StrUtil.isEmpty(input)) { + return input; + } + final char[] c = input.toCharArray(); + for (int i = 0; i < c.length; i++) { + if (null != notConvertSet && notConvertSet.contains(c[i])) { + // 跳过不替换的字符 + continue; + } + + if (c[i] == CharUtil.SPACE) { + c[i] = '\u3000'; + } else if (c[i] < '\177') { + c[i] = (char) (c[i] + 65248); + } + } + return new String(c); + } + + /** + * 全角转半角 + * + * @param input String. + * @return 半角字符串 + */ + public static String toDBC(String input) { + return toDBC(input, null); + } + + /** + * 替换全角为半角 + * + * @param text 文本 + * @param notConvertSet 不替换的字符集合 + * @return 替换后的字符 + */ + public static String toDBC(String text, Set notConvertSet) { + if (StrUtil.isBlank(text)) { + return text; + } + final char[] c = text.toCharArray(); + for (int i = 0; i < c.length; i++) { + if (null != notConvertSet && notConvertSet.contains(c[i])) { + // 跳过不替换的字符 + continue; + } + + if (c[i] == '\u3000' || c[i] == '\u00a0' || c[i] == '\u2007' || c[i] == '\u202F') { + // \u3000是中文全角空格,\u00a0、\u2007、\u202F是不间断空格 + c[i] = ' '; + } else if (c[i] > '\uFF00' && c[i] < '\uFF5F') { + c[i] = (char) (c[i] - 65248); + } + } + + return new String(c); + } + + // --------------------------------------------------------------------- hex + + /** + * 字符串转换成十六进制字符串,结果为小写 + * + * @param str 待转换的ASCII字符串 + * @param charset 编码 + * @return 16进制字符串 + * @see HexUtil#encodeHexStr(String, Charset) + */ + public static String toHex(String str, Charset charset) { + return HexUtil.encodeHexStr(str, charset); + } + + /** + * byte数组转16进制串 + * + * @param bytes 被转换的byte数组 + * @return 转换后的值 + * @see HexUtil#encodeHexStr(byte[]) + */ + public static String toHex(byte[] bytes) { + return HexUtil.encodeHexStr(bytes); + } + + /** + * Hex字符串转换为Byte值 + * + * @param src Byte字符串,每个Byte之间没有分隔符 + * @return byte[] + * @see HexUtil#decodeHex(char[]) + */ + public static byte[] hexToBytes(String src) { + return HexUtil.decodeHex(src.toCharArray()); + } + + /** + * 十六进制转换字符串 + * + * @param hexStr Byte字符串(Byte之间无分隔符 如:[616C6B]) + * @param charset 编码 {@link Charset} + * @return 对应的字符串 + * @see HexUtil#decodeHexStr(String, Charset) + * @since 4.1.11 + */ + public static String hexToStr(String hexStr, Charset charset) { + return HexUtil.decodeHexStr(hexStr, charset); + } + + /** + * String的字符串转换成unicode的String + * + * @param strText 全角字符串 + * @return String 每个unicode之间无分隔符 + * @see UnicodeUtil#toUnicode(String) + */ + public static String strToUnicode(String strText) { + return UnicodeUtil.toUnicode(strText); + } + + /** + * unicode的String转换成String的字符串 + * + * @param unicode Unicode符 + * @return String 字符串 + * @see UnicodeUtil#toString(String) + */ + public static String unicodeToStr(String unicode) { + return UnicodeUtil.toString(unicode); + } + + /** + * 给定字符串转换字符编码
+ * 如果参数为空,则返回原字符串,不报错。 + * + * @param str 被转码的字符串 + * @param sourceCharset 原字符集 + * @param destCharset 目标字符集 + * @return 转换后的字符串 + * @see CharsetUtil#convert(String, String, String) + */ + public static String convertCharset(String str, String sourceCharset, String destCharset) { + if (StrUtil.hasBlank(str, sourceCharset, destCharset)) { + return str; + } + + return CharsetUtil.convert(str, sourceCharset, destCharset); + } + + /** + * 转换时间单位 + * + * @param sourceDuration 时长 + * @param sourceUnit 源单位 + * @param destUnit 目标单位 + * @return 目标单位的时长 + */ + public static long convertTime(long sourceDuration, TimeUnit sourceUnit, TimeUnit destUnit) { + Assert.notNull(sourceUnit, "sourceUnit is null !"); + Assert.notNull(destUnit, "destUnit is null !"); + return destUnit.convert(sourceDuration, sourceUnit); + } + + // --------------------------------------------------------------- 原始包装类型转换 + + /** + * 原始类转为包装类,非原始类返回原类 + * + * @param clazz 原始类 + * @return 包装类 + * @see BasicType#wrap(Class) + * @see BasicType#wrap(Class) + */ + public static Class wrap(Class clazz) { + return BasicType.wrap(clazz); + } + + /** + * 包装类转为原始类,非包装类返回原类 + * + * @param clazz 包装类 + * @return 原始类 + * @see BasicType#unWrap(Class) + * @see BasicType#unWrap(Class) + */ + public static Class unWrap(Class clazz) { + return BasicType.unWrap(clazz); + } + + // -------------------------------------------------------------------------- 数字和英文转换 + + /** + * 将阿拉伯数字转为英文表达方式 + * + * @param number {@link Number}对象 + * @return 英文表达式 + * @since 3.0.9 + */ + public static String numberToWord(Number number) { + return NumberWordFormatter.format(number); + } + + /** + * 将阿拉伯数字转为精简表示形式,例如: + * + *
+	 *     1200 -》 1.2k
+	 * 
+ * + * @param number {@link Number}对象 + * @return 英文表达式 + * @since 5.5.9 + */ + public static String numberToSimple(Number number) { + return NumberWordFormatter.formatSimple(number.longValue()); + } + + /** + * 将阿拉伯数字转为中文表达方式 + * + * @param number 数字 + * @param isUseTraditional 是否使用繁体字(金额形式) + * @return 中文 + * @since 3.2.3 + */ + public static String numberToChinese(double number, boolean isUseTraditional) { + return NumberChineseFormatter.format(number, isUseTraditional); + } + + /** + * 数字中文表示形式转数字 + *
    + *
  • 一百一十二 -》 112
  • + *
  • 一千零一十二 -》 1012
  • + *
+ * + * @param number 数字中文表示 + * @return 数字 + * @since 5.6.0 + */ + public static int chineseToNumber(String number) { + return NumberChineseFormatter.chineseToNumber(number); + } + + /** + * 金额转为中文形式 + * + * @param n 数字 + * @return 中文大写数字 + * @since 3.2.3 + */ + public static String digitToChinese(Number n) { + if (null == n) { + return "零"; + } + return NumberChineseFormatter.format(n.doubleValue(), true, true); + } + + /** + * 中文大写数字金额转换为数字,返回结果以元为单位的BigDecimal类型数字
+ * 如: + * “陆万柒仟伍佰伍拾陆元叁角贰分”返回“67556.32” + * “叁角贰分”返回“0.32” + * + * @param chineseMoneyAmount 中文大写数字金额 + * @return 返回结果以元为单位的BigDecimal类型数字 + * @since 5.8.5 + */ + public static BigDecimal chineseMoneyToNumber(String chineseMoneyAmount) { + return NumberChineseFormatter.chineseMoneyToNumber(chineseMoneyAmount); + } + + // -------------------------------------------------------------------------- 数字转换 + + /** + * int转byte + * + * @param intValue int值 + * @return byte值 + * @since 3.2.0 + */ + public static byte intToByte(int intValue) { + return (byte) intValue; + } + + /** + * byte转无符号int + * + * @param byteValue byte值 + * @return 无符号int值 + * @since 3.2.0 + */ + public static int byteToUnsignedInt(byte byteValue) { + // Java 总是把 byte 当做有符处理;我们可以通过将其和 0xFF 进行二进制与得到它的无符值 + return byteValue & 0xFF; + } + + /** + * byte数组转short
+ * 默认以小端序转换 + * + * @param bytes byte数组 + * @return short值 + * @since 5.6.3 + */ + public static short bytesToShort(byte[] bytes) { + return ByteUtil.bytesToShort(bytes); + } + + /** + * short转byte数组
+ * 默认以小端序转换 + * + * @param shortValue short值 + * @return byte数组 + * @since 5.6.3 + */ + public static byte[] shortToBytes(short shortValue) { + return ByteUtil.shortToBytes(shortValue); + } + + /** + * byte[]转int值
+ * 默认以小端序转换 + * + * @param bytes byte数组 + * @return int值 + * @since 5.6.3 + */ + public static int bytesToInt(byte[] bytes) { + return ByteUtil.bytesToInt(bytes); + } + + /** + * int转byte数组
+ * 默认以小端序转换 + * + * @param intValue int值 + * @return byte数组 + * @since 5.6.3 + */ + public static byte[] intToBytes(int intValue) { + return ByteUtil.intToBytes(intValue); + } + + /** + * long转byte数组
+ * 默认以小端序转换
+ * from: https://stackoverflow.com/questions/4485128/how-do-i-convert-long-to-byte-and-back-in-java + * + * @param longValue long值 + * @return byte数组 + * @since 5.6.3 + */ + public static byte[] longToBytes(long longValue) { + return ByteUtil.longToBytes(longValue); + } + + /** + * byte数组转long
+ * 默认以小端序转换
+ * from: https://stackoverflow.com/questions/4485128/how-do-i-convert-long-to-byte-and-back-in-java + * + * @param bytes byte数组 + * @return long值 + * @since 5.6.3 + */ + public static long bytesToLong(byte[] bytes) { + return ByteUtil.bytesToLong(bytes); + } +} diff --git a/src/main/java/cn/hutool/core/convert/ConvertException.java b/src/main/java/cn/hutool/core/convert/ConvertException.java new file mode 100644 index 0000000..e012014 --- /dev/null +++ b/src/main/java/cn/hutool/core/convert/ConvertException.java @@ -0,0 +1,32 @@ +package cn.hutool.core.convert; + +import cn.hutool.core.exceptions.ExceptionUtil; +import cn.hutool.core.util.StrUtil; + +/** + * 转换异常 + * @author xiaoleilu + */ +public class ConvertException extends RuntimeException{ + private static final long serialVersionUID = 4730597402855274362L; + + public ConvertException(Throwable e) { + super(ExceptionUtil.getMessage(e), e); + } + + public ConvertException(String message) { + super(message); + } + + public ConvertException(String messageTemplate, Object... params) { + super(StrUtil.format(messageTemplate, params)); + } + + public ConvertException(String message, Throwable throwable) { + super(message, throwable); + } + + public ConvertException(Throwable throwable, String messageTemplate, Object... params) { + super(StrUtil.format(messageTemplate, params), throwable); + } +} diff --git a/src/main/java/cn/hutool/core/convert/Converter.java b/src/main/java/cn/hutool/core/convert/Converter.java new file mode 100644 index 0000000..6b1319f --- /dev/null +++ b/src/main/java/cn/hutool/core/convert/Converter.java @@ -0,0 +1,43 @@ +package cn.hutool.core.convert; + +/** + * 转换器接口,实现类型转换 + * + * @param 转换到的目标类型 + * @author Looly + */ +public interface Converter { + + /** + * 转换为指定类型
+ * 如果类型无法确定,将读取默认值的类型做为目标类型 + * + * @param value 原始值 + * @param defaultValue 默认值 + * @return 转换后的值 + * @throws IllegalArgumentException 无法确定目标类型,且默认值为{@code null},无法确定类型 + */ + T convert(Object value, T defaultValue) throws IllegalArgumentException; + + /** + * 转换值为指定类型,可选是否不抛异常转换
+ * 当转换失败时返回默认值 + * + * @param value 值 + * @param defaultValue 默认值 + * @param quietly 是否静默转换,true不抛异常 + * @return 转换后的值 + * @since 5.8.0 + * @see #convert(Object, Object) + */ + default T convertWithCheck(Object value, T defaultValue, boolean quietly) { + try { + return convert(value, defaultValue); + } catch (Exception e) { + if(quietly){ + return defaultValue; + } + throw e; + } + } +} diff --git a/src/main/java/cn/hutool/core/convert/ConverterRegistry.java b/src/main/java/cn/hutool/core/convert/ConverterRegistry.java new file mode 100644 index 0000000..8b86912 --- /dev/null +++ b/src/main/java/cn/hutool/core/convert/ConverterRegistry.java @@ -0,0 +1,405 @@ +package cn.hutool.core.convert; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.convert.impl.*; +import cn.hutool.core.date.DateTime; +import cn.hutool.core.lang.Opt; +import cn.hutool.core.lang.TypeReference; +import cn.hutool.core.map.SafeConcurrentHashMap; +import cn.hutool.core.util.*; + +import java.io.Serializable; +import java.lang.ref.SoftReference; +import java.lang.ref.WeakReference; +import java.lang.reflect.Type; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.net.URI; +import java.net.URL; +import java.nio.charset.Charset; +import java.nio.file.Path; +import java.time.*; +import java.time.temporal.TemporalAccessor; +import java.util.*; +import java.util.concurrent.atomic.*; + +/** + * 转换器登记中心 + *

+ * 将各种类型Convert对象放入登记中心,通过convert方法查找目标类型对应的转换器,将被转换对象转换之。 + *

+ *

+ * 在此类中,存放着默认转换器和自定义转换器,默认转换器是Hutool中预定义的一些转换器,自定义转换器存放用户自定的转换器。 + *

+ * + * @author Looly + */ +public class ConverterRegistry implements Serializable { + private static final long serialVersionUID = 1L; + + /** + * 默认类型转换器 + */ + private Map> defaultConverterMap; + /** + * 用户自定义类型转换器 + */ + private volatile Map> customConverterMap; + + /** + * 类级的内部类,也就是静态的成员式内部类,该内部类的实例与外部类的实例 没有绑定关系,而且只有被调用到才会装载,从而实现了延迟加载 + */ + private static class SingletonHolder { + /** + * 静态初始化器,由JVM来保证线程安全 + */ + private static final ConverterRegistry INSTANCE = new ConverterRegistry(); + } + + /** + * 获得单例的 ConverterRegistry + * + * @return ConverterRegistry + */ + public static ConverterRegistry getInstance() { + return SingletonHolder.INSTANCE; + } + + /** + * 构造 + */ + public ConverterRegistry() { + defaultConverter(); + putCustomBySpi(); + } + + /** + * 使用SPI加载转换器 + */ + private void putCustomBySpi() { + ServiceLoaderUtil.load(Converter.class).forEach(converter -> { + try { + Type type = TypeUtil.getTypeArgument(ClassUtil.getClass(converter)); + if (null != type) { + putCustom(type, converter); + } + } catch (Exception e) { + // 忽略注册失败的 + } + }); + } + + /** + * 登记自定义转换器 + * + * @param type 转换的目标类型 + * @param converterClass 转换器类,必须有默认构造方法 + * @return ConverterRegistry + */ + public ConverterRegistry putCustom(Type type, Class> converterClass) { + return putCustom(type, ReflectUtil.newInstance(converterClass)); + } + + /** + * 登记自定义转换器 + * + * @param type 转换的目标类型 + * @param converter 转换器 + * @return ConverterRegistry + */ + public ConverterRegistry putCustom(Type type, Converter converter) { + if (null == customConverterMap) { + synchronized (this) { + if (null == customConverterMap) { + customConverterMap = new SafeConcurrentHashMap<>(); + } + } + } + customConverterMap.put(type, converter); + return this; + } + + /** + * 获得转换器
+ * + * @param 转换的目标类型 + * @param type 类型 + * @param isCustomFirst 是否自定义转换器优先 + * @return 转换器 + */ + public Converter getConverter(Type type, boolean isCustomFirst) { + Converter converter; + if (isCustomFirst) { + converter = this.getCustomConverter(type); + if (null == converter) { + converter = this.getDefaultConverter(type); + } + } else { + converter = this.getDefaultConverter(type); + if (null == converter) { + converter = this.getCustomConverter(type); + } + } + return converter; + } + + /** + * 获得默认转换器 + * + * @param 转换的目标类型(转换器转换到的类型) + * @param type 类型 + * @return 转换器 + */ + @SuppressWarnings("unchecked") + public Converter getDefaultConverter(Type type) { + return (null == defaultConverterMap) ? null : (Converter) defaultConverterMap.get(type); + } + + /** + * 获得自定义转换器 + * + * @param 转换的目标类型(转换器转换到的类型) + * @param type 类型 + * @return 转换器 + */ + @SuppressWarnings("unchecked") + public Converter getCustomConverter(Type type) { + return (null == customConverterMap) ? null : (Converter) customConverterMap.get(type); + } + + /** + * 转换值为指定类型 + * + * @param 转换的目标类型(转换器转换到的类型) + * @param type 类型目标 + * @param value 被转换值 + * @param defaultValue 默认值 + * @param isCustomFirst 是否自定义转换器优先 + * @return 转换后的值 + * @throws ConvertException 转换器不存在 + */ + @SuppressWarnings("unchecked") + public T convert(Type type, Object value, T defaultValue, boolean isCustomFirst) throws ConvertException { + if (TypeUtil.isUnknown(type) && null == defaultValue) { + // 对于用户不指定目标类型的情况,返回原值 + return (T) value; + } + if (ObjectUtil.isNull(value)) { + return defaultValue; + } + if (TypeUtil.isUnknown(type)) { + type = defaultValue.getClass(); + } + + if (type instanceof TypeReference) { + type = ((TypeReference) type).getType(); + } + + // 自定义对象转换 + if(value instanceof TypeConverter){ + return ObjUtil.defaultIfNull((T) ((TypeConverter) value).convert(type, value), defaultValue); + } + + // 标准转换器 + final Converter converter = getConverter(type, isCustomFirst); + if (null != converter) { + return converter.convert(value, defaultValue); + } + + Class rowType = (Class) TypeUtil.getClass(type); + if (null == rowType) { + if (null != defaultValue) { + rowType = (Class) defaultValue.getClass(); + } else { + // 无法识别的泛型类型,按照Object处理 + return (T) value; + } + } + + // 特殊类型转换,包括Collection、Map、强转、Array等 + final T result = convertSpecial(type, rowType, value, defaultValue); + if (null != result) { + return result; + } + + // 尝试转Bean + if (BeanUtil.isBean(rowType)) { + return new BeanConverter(type).convert(value, defaultValue); + } + + // 无法转换 + throw new ConvertException("Can not Converter from [{}] to [{}]", value.getClass().getName(), type.getTypeName()); + } + + /** + * 转换值为指定类型
+ * 自定义转换器优先 + * + * @param 转换的目标类型(转换器转换到的类型) + * @param type 类型 + * @param value 值 + * @param defaultValue 默认值 + * @return 转换后的值 + * @throws ConvertException 转换器不存在 + */ + public T convert(Type type, Object value, T defaultValue) throws ConvertException { + return convert(type, value, defaultValue, true); + } + + /** + * 转换值为指定类型 + * + * @param 转换的目标类型(转换器转换到的类型) + * @param type 类型 + * @param value 值 + * @return 转换后的值,默认为{@code null} + * @throws ConvertException 转换器不存在 + */ + public T convert(Type type, Object value) throws ConvertException { + return convert(type, value, null); + } + + // ----------------------------------------------------------- Private method start + + /** + * 特殊类型转换
+ * 包括: + * + *
+	 * Collection
+	 * Map
+	 * 强转(无需转换)
+	 * 数组
+	 * 
+ * + * @param 转换的目标类型(转换器转换到的类型) + * @param type 类型 + * @param value 值 + * @param defaultValue 默认值 + * @return 转换后的值 + */ + @SuppressWarnings("unchecked") + private T convertSpecial(Type type, Class rowType, Object value, T defaultValue) { + if (null == rowType) { + return null; + } + + // 集合转换(不可以默认强转) + if (Collection.class.isAssignableFrom(rowType)) { + final CollectionConverter collectionConverter = new CollectionConverter(type); + return (T) collectionConverter.convert(value, (Collection) defaultValue); + } + + // Map类型(不可以默认强转) + if (Map.class.isAssignableFrom(rowType)) { + final MapConverter mapConverter = new MapConverter(type); + return (T) mapConverter.convert(value, (Map) defaultValue); + } + + // 默认强转 + if (rowType.isInstance(value)) { + return (T) value; + } + + // 枚举转换 + if (rowType.isEnum()) { + return (T) new EnumConverter(rowType).convert(value, defaultValue); + } + + // 数组转换 + if (rowType.isArray()) { + final ArrayConverter arrayConverter = new ArrayConverter(rowType); + return (T) arrayConverter.convert(value, defaultValue); + } + + // 表示非需要特殊转换的对象 + return null; + } + + /** + * 注册默认转换器 + * + * @return 转换器 + */ + private ConverterRegistry defaultConverter() { + defaultConverterMap = new SafeConcurrentHashMap<>(); + + // 原始类型转换器 + defaultConverterMap.put(int.class, new PrimitiveConverter(int.class)); + defaultConverterMap.put(long.class, new PrimitiveConverter(long.class)); + defaultConverterMap.put(byte.class, new PrimitiveConverter(byte.class)); + defaultConverterMap.put(short.class, new PrimitiveConverter(short.class)); + defaultConverterMap.put(float.class, new PrimitiveConverter(float.class)); + defaultConverterMap.put(double.class, new PrimitiveConverter(double.class)); + defaultConverterMap.put(char.class, new PrimitiveConverter(char.class)); + defaultConverterMap.put(boolean.class, new PrimitiveConverter(boolean.class)); + + // 包装类转换器 + defaultConverterMap.put(Number.class, new NumberConverter()); + defaultConverterMap.put(Integer.class, new NumberConverter(Integer.class)); + defaultConverterMap.put(AtomicInteger.class, new NumberConverter(AtomicInteger.class));// since 3.0.8 + defaultConverterMap.put(Long.class, new NumberConverter(Long.class)); + defaultConverterMap.put(LongAdder.class, new NumberConverter(LongAdder.class)); + defaultConverterMap.put(AtomicLong.class, new NumberConverter(AtomicLong.class));// since 3.0.8 + defaultConverterMap.put(Byte.class, new NumberConverter(Byte.class)); + defaultConverterMap.put(Short.class, new NumberConverter(Short.class)); + defaultConverterMap.put(Float.class, new NumberConverter(Float.class)); + defaultConverterMap.put(Double.class, new NumberConverter(Double.class)); + defaultConverterMap.put(DoubleAdder.class, new NumberConverter(DoubleAdder.class)); + defaultConverterMap.put(Character.class, new CharacterConverter()); + defaultConverterMap.put(Boolean.class, new BooleanConverter()); + defaultConverterMap.put(AtomicBoolean.class, new AtomicBooleanConverter());// since 3.0.8 + defaultConverterMap.put(BigDecimal.class, new NumberConverter(BigDecimal.class)); + defaultConverterMap.put(BigInteger.class, new NumberConverter(BigInteger.class)); + defaultConverterMap.put(CharSequence.class, new StringConverter()); + defaultConverterMap.put(String.class, new StringConverter()); + + // URI and URL + defaultConverterMap.put(URI.class, new URIConverter()); + defaultConverterMap.put(URL.class, new URLConverter()); + + // 日期时间 + defaultConverterMap.put(Calendar.class, new CalendarConverter()); + defaultConverterMap.put(java.util.Date.class, new DateConverter(java.util.Date.class)); + defaultConverterMap.put(DateTime.class, new DateConverter(DateTime.class)); + + // 日期时间 JDK8+(since 5.0.0) + defaultConverterMap.put(TemporalAccessor.class, new TemporalAccessorConverter(Instant.class)); + defaultConverterMap.put(Instant.class, new TemporalAccessorConverter(Instant.class)); + defaultConverterMap.put(LocalDateTime.class, new TemporalAccessorConverter(LocalDateTime.class)); + defaultConverterMap.put(LocalDate.class, new TemporalAccessorConverter(LocalDate.class)); + defaultConverterMap.put(LocalTime.class, new TemporalAccessorConverter(LocalTime.class)); + defaultConverterMap.put(ZonedDateTime.class, new TemporalAccessorConverter(ZonedDateTime.class)); + defaultConverterMap.put(OffsetDateTime.class, new TemporalAccessorConverter(OffsetDateTime.class)); + defaultConverterMap.put(OffsetTime.class, new TemporalAccessorConverter(OffsetTime.class)); + defaultConverterMap.put(DayOfWeek.class, new TemporalAccessorConverter(DayOfWeek.class)); + defaultConverterMap.put(Month.class, new TemporalAccessorConverter(Month.class)); + defaultConverterMap.put(MonthDay.class, new TemporalAccessorConverter(MonthDay.class)); + defaultConverterMap.put(Period.class, new PeriodConverter()); + defaultConverterMap.put(Duration.class, new DurationConverter()); + + // Reference + defaultConverterMap.put(WeakReference.class, new ReferenceConverter(WeakReference.class));// since 3.0.8 + defaultConverterMap.put(SoftReference.class, new ReferenceConverter(SoftReference.class));// since 3.0.8 + defaultConverterMap.put(AtomicReference.class, new AtomicReferenceConverter());// since 3.0.8 + + //AtomicXXXArray,since 5.4.5 + defaultConverterMap.put(AtomicIntegerArray.class, new AtomicIntegerArrayConverter()); + defaultConverterMap.put(AtomicLongArray.class, new AtomicLongArrayConverter()); + + // 其它类型 + defaultConverterMap.put(Class.class, new ClassConverter()); + defaultConverterMap.put(TimeZone.class, new TimeZoneConverter()); + defaultConverterMap.put(Locale.class, new LocaleConverter()); + defaultConverterMap.put(Charset.class, new CharsetConverter()); + defaultConverterMap.put(Path.class, new PathConverter()); + defaultConverterMap.put(Currency.class, new CurrencyConverter());// since 3.0.8 + defaultConverterMap.put(UUID.class, new UUIDConverter());// since 4.0.10 + defaultConverterMap.put(StackTraceElement.class, new StackTraceElementConverter());// since 4.5.2 + defaultConverterMap.put(Optional.class, new OptionalConverter());// since 5.0.0 + defaultConverterMap.put(Opt.class, new OptConverter());// since 5.7.16 + + return this; + } + // ----------------------------------------------------------- Private method end +} diff --git a/src/main/java/cn/hutool/core/convert/NumberChineseFormatter.java b/src/main/java/cn/hutool/core/convert/NumberChineseFormatter.java new file mode 100644 index 0000000..ce13cad --- /dev/null +++ b/src/main/java/cn/hutool/core/convert/NumberChineseFormatter.java @@ -0,0 +1,617 @@ +package cn.hutool.core.convert; + +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.NumberUtil; +import cn.hutool.core.util.StrUtil; + +import java.math.BigDecimal; +import java.math.RoundingMode; + +/** + * 数字转中文类
+ * 包括: + *
+ * 1. 数字转中文大写形式,比如一百二十一
+ * 2. 数字转金额用的大写形式,比如:壹佰贰拾壹
+ * 3. 转金额形式,比如:壹佰贰拾壹整
+ * 
+ * + * @author fanqun, looly + **/ +public class NumberChineseFormatter { + + /** + * 中文形式,奇数位置是简体,偶数位置是记账繁体,0共用
+ * 使用混合数组提高效率和数组复用 + **/ + private static final char[] DIGITS = {'零', '一', '壹', '二', '贰', '三', '叁', '四', '肆', '五', '伍', + '六', '陆', '七', '柒', '八', '捌', '九', '玖'}; + + /** + * 汉字转阿拉伯数字的 + */ + private static final ChineseUnit[] CHINESE_NAME_VALUE = { + new ChineseUnit(' ', 1, false), + new ChineseUnit('十', 10, false), + new ChineseUnit('拾', 10, false), + new ChineseUnit('百', 100, false), + new ChineseUnit('佰', 100, false), + new ChineseUnit('千', 1000, false), + new ChineseUnit('仟', 1000, false), + new ChineseUnit('万', 1_0000, true), + new ChineseUnit('亿', 1_0000_0000, true), + }; + + /** + * 阿拉伯数字转换成中文,小数点后四舍五入保留两位. 使用于整数、小数的转换. + * + * @param amount 数字 + * @param isUseTraditional 是否使用繁体 + * @return 中文 + */ + public static String format(double amount, boolean isUseTraditional) { + return format(amount, isUseTraditional, false); + } + + /** + * 阿拉伯数字转换成中文. + * + *

主要是对发票票面金额转换的扩展 + *

如:-12.32 + *

发票票面转换为:(负数)壹拾贰圆叁角贰分 + *

而非:负壹拾贰元叁角贰分 + *

共两点不同:1、(负数) 而非 负;2、圆 而非 元 + * 2022/3/9 + * + * @param amount 数字 + * @param isUseTraditional 是否使用繁体 + * @param isMoneyMode 是否金额模式 + * @param negativeName 负号转换名称 如:负、(负数) + * @param unitName 单位名称 如:元、圆 + * @return java.lang.String + * @author machuanpeng + * @since 5.7.23 + */ + public static String format(double amount, boolean isUseTraditional, boolean isMoneyMode, String negativeName, String unitName) { + if (0 == amount) { + return "零"; + } + Assert.checkBetween(amount, -99_9999_9999_9999.99, 99_9999_9999_9999.99, + "Number support only: (-99999999999999.99 ~ 99999999999999.99)!"); + + final StringBuilder chineseStr = new StringBuilder(); + + // 负数 + if (amount < 0) { + chineseStr.append(StrUtil.isNullOrUndefined(negativeName) ? "负" : negativeName); + amount = -amount; + } + + long yuan = Math.round(amount * 100); + final int fen = (int) (yuan % 10); + yuan = yuan / 10; + final int jiao = (int) (yuan % 10); + yuan = yuan / 10; + + // 元 + if (!isMoneyMode || 0 != yuan) { + // 金额模式下,无需“零元” + chineseStr.append(longToChinese(yuan, isUseTraditional)); + if (isMoneyMode) { + chineseStr.append(StrUtil.isNullOrUndefined(unitName) ? "元" : unitName); + } + } + + if (0 == jiao && 0 == fen) { + //无小数部分的金额结尾 + if (isMoneyMode) { + chineseStr.append("整"); + } + return chineseStr.toString(); + } + + // 小数部分 + if (!isMoneyMode) { + chineseStr.append("点"); + } + + // 角 + if (0 == yuan && 0 == jiao) { + // 元和角都为0时,只有非金额模式下补“零” + if (!isMoneyMode) { + chineseStr.append("零"); + } + } else { + chineseStr.append(numberToChinese(jiao, isUseTraditional)); + if (isMoneyMode && 0 != jiao) { + chineseStr.append("角"); + } + } + + // 分 + if (0 != fen) { + chineseStr.append(numberToChinese(fen, isUseTraditional)); + if (isMoneyMode) { + chineseStr.append("分"); + } + } + + return chineseStr.toString(); + } + + /** + * 阿拉伯数字转换成中文,小数点后四舍五入保留两位. 使用于整数、小数的转换. + * + * @param amount 数字 + * @param isUseTraditional 是否使用繁体 + * @param isMoneyMode 是否为金额模式 + * @return 中文 + */ + public static String format(double amount, boolean isUseTraditional, boolean isMoneyMode) { + return format(amount, isUseTraditional, isMoneyMode, "负", "元"); + } + + /** + * 阿拉伯数字(支持正负整数)转换成中文 + * + * @param amount 数字 + * @param isUseTraditional 是否使用繁体 + * @return 中文 + * @since 5.7.17 + */ + public static String format(long amount, boolean isUseTraditional) { + if (0 == amount) { + return "零"; + } + Assert.checkBetween(amount, -99_9999_9999_9999.99, 99_9999_9999_9999.99, + "Number support only: (-99999999999999.99 ~ 99999999999999.99)!"); + + final StringBuilder chineseStr = new StringBuilder(); + + // 负数 + if (amount < 0) { + chineseStr.append("负"); + amount = -amount; + } + + chineseStr.append(longToChinese(amount, isUseTraditional)); + return chineseStr.toString(); + } + + /** + * 阿拉伯数字(支持正负整数)四舍五入后转换成中文节权位简洁计数单位,例如 -5_5555 =》 -5.56万 + * + * @param amount 数字 + * @return 中文 + */ + public static String formatSimple(long amount) { + if (amount < 1_0000 && amount > -1_0000) { + return String.valueOf(amount); + } + String res; + if (amount < 1_0000_0000 && amount > -1_0000_0000) { + res = NumberUtil.div(amount, 1_0000, 2) + "万"; + } else if (amount < 1_0000_0000_0000L && amount > -1_0000_0000_0000L) { + res = NumberUtil.div(amount, 1_0000_0000, 2) + "亿"; + } else { + res = NumberUtil.div(amount, 1_0000_0000_0000L, 2) + "万亿"; + } + return res; + } + + /** + * 格式化-999~999之间的数字
+ * 这个方法显示10~19以下的数字时使用"十一"而非"一十一"。 + * + * @param amount 数字 + * @param isUseTraditional 是否使用繁体 + * @return 中文 + * @since 5.7.17 + */ + public static String formatThousand(int amount, boolean isUseTraditional) { + Assert.checkBetween(amount, -999, 999, "Number support only: (-999 ~ 999)!"); + + final String chinese = thousandToChinese(amount, isUseTraditional); + if (amount < 20 && amount >= 10) { + // "十一"而非"一十一" + return chinese.substring(1); + } + return chinese; + } + + /** + * 数字字符转中文,非数字字符原样返回 + * + * @param c 数字字符 + * @param isUseTraditional 是否繁体 + * @return 中文字符 + * @since 5.3.9 + */ + public static String numberCharToChinese(char c, boolean isUseTraditional) { + if (c < '0' || c > '9') { + return String.valueOf(c); + } + return String.valueOf(numberToChinese(c - '0', isUseTraditional)); + } + + /** + * 中文大写数字金额转换为数字,返回结果以元为单位的BigDecimal类型数字 + * 如: + * “陆万柒仟伍佰伍拾陆元叁角贰分”返回“67556.32” + * “叁角贰分”返回“0.32” + * + * @param chineseMoneyAmount 中文大写数字金额 + * @return 返回结果以元为单位的BigDecimal类型数字 + */ + @SuppressWarnings("ConstantConditions") + public static BigDecimal chineseMoneyToNumber(String chineseMoneyAmount){ + if(StrUtil.isBlank(chineseMoneyAmount)){ + return null; + } + + int yi = chineseMoneyAmount.indexOf("元"); + if(yi == -1){ + yi = chineseMoneyAmount.indexOf("圆"); + } + final int ji = chineseMoneyAmount.indexOf("角"); + final int fi = chineseMoneyAmount.indexOf("分"); + + // 先找到单位为元的数字 + String yStr = null; + if(yi > 0) { + yStr = chineseMoneyAmount.substring(0, yi); + } + + // 再找到单位为角的数字 + String jStr = null; + if(ji > 0){ + if(yi >= 0){ + //前面有元,角肯定要在元后面 + if(ji > yi){ + jStr = chineseMoneyAmount.substring(yi+1, ji); + } + }else{ + //没有元,只有角 + jStr = chineseMoneyAmount.substring(0, ji); + } + } + + // 再找到单位为分的数字 + String fStr = null; + if(fi > 0){ + if(ji >= 0){ + //有角,分肯定在角后面 + if(fi > ji){ + fStr = chineseMoneyAmount.substring(ji+1, fi); + } + }else if(yi > 0){ + //没有角,有元,那就坐元后面找 + if(fi > yi){ + fStr = chineseMoneyAmount.substring(yi+1, fi); + } + }else { + //没有元、角,只有分 + fStr = chineseMoneyAmount.substring(0, fi); + } + } + + //元、角、分 + int y = 0, j = 0, f = 0; + if(StrUtil.isNotBlank(yStr)) { + y = NumberChineseFormatter.chineseToNumber(yStr); + } + if(StrUtil.isNotBlank(jStr)){ + j = NumberChineseFormatter.chineseToNumber(jStr); + } + if(StrUtil.isNotBlank(fStr)){ + f = NumberChineseFormatter.chineseToNumber(fStr); + } + + BigDecimal amount = new BigDecimal(y); + amount = amount.add(BigDecimal.valueOf(j).divide(BigDecimal.TEN, 2, RoundingMode.HALF_UP)); + amount = amount.add(BigDecimal.valueOf(f).divide(BigDecimal.valueOf(100), 2, RoundingMode.HALF_UP)); + return amount; + } + + /** + * 阿拉伯数字整数部分转换成中文,只支持正数 + * + * @param amount 数字 + * @param isUseTraditional 是否使用繁体 + * @return 中文 + */ + private static String longToChinese(long amount, boolean isUseTraditional) { + if (0 == amount) { + return "零"; + } + + //将数字以万为单位分为多份 + int[] parts = new int[4]; + for (int i = 0; amount != 0; i++) { + parts[i] = (int) (amount % 10000); + amount = amount / 10000; + } + + final StringBuilder chineseStr = new StringBuilder(); + int partValue; + String partChinese; + + // 千 + partValue = parts[0]; + if (partValue > 0) { + partChinese = thousandToChinese(partValue, isUseTraditional); + chineseStr.insert(0, partChinese); + + if (partValue < 1000) { + // 和万位之间空0,则补零,如一万零三百 + addPreZero(chineseStr); + } + } + + // 万 + partValue = parts[1]; + if (partValue > 0) { + if ((partValue % 10 == 0 && parts[0] > 0)) { + // 如果"万"的个位是0,则补零,如十万零八千 + addPreZero(chineseStr); + } + partChinese = thousandToChinese(partValue, isUseTraditional); + chineseStr.insert(0, partChinese + "万"); + + if (partValue < 1000) { + // 和亿位之间空0,则补零,如一亿零三百万 + addPreZero(chineseStr); + } + } else { + addPreZero(chineseStr); + } + + // 亿 + partValue = parts[2]; + if (partValue > 0) { + if ((partValue % 10 == 0 && parts[1] > 0)) { + // 如果"万"的个位是0,则补零,如十万零八千 + addPreZero(chineseStr); + } + + partChinese = thousandToChinese(partValue, isUseTraditional); + chineseStr.insert(0, partChinese + "亿"); + + if (partValue < 1000) { + // 和万亿位之间空0,则补零,如一万亿零三百亿 + addPreZero(chineseStr); + } + } else { + addPreZero(chineseStr); + } + + // 万亿 + partValue = parts[3]; + if (partValue > 0) { + if (parts[2] == 0) { + chineseStr.insert(0, "亿"); + } + partChinese = thousandToChinese(partValue, isUseTraditional); + chineseStr.insert(0, partChinese + "万"); + } + + if (StrUtil.isNotEmpty(chineseStr) && '零' == chineseStr.charAt(0)) { + return chineseStr.substring(1); + } + + return chineseStr.toString(); + } + + /** + * 把一个 0~9999 之间的整数转换为汉字的字符串,如果是 0 则返回 "" + * + * @param amountPart 数字部分 + * @param isUseTraditional 是否使用繁体单位 + * @return 转换后的汉字 + */ + private static String thousandToChinese(int amountPart, boolean isUseTraditional) { + if (amountPart == 0) { + // issue#I4R92H@Gitee + return String.valueOf(DIGITS[0]); + } + + int temp = amountPart; + + StringBuilder chineseStr = new StringBuilder(); + boolean lastIsZero = true; // 在从低位往高位循环时,记录上一位数字是不是 0 + for (int i = 0; temp > 0; i++) { + int digit = temp % 10; + if (digit == 0) { // 取到的数字为 0 + if (!lastIsZero) { + // 前一个数字不是 0,则在当前汉字串前加“零”字; + chineseStr.insert(0, "零"); + } + lastIsZero = true; + } else { // 取到的数字不是 0 + chineseStr.insert(0, numberToChinese(digit, isUseTraditional) + getUnitName(i, isUseTraditional)); + lastIsZero = false; + } + temp = temp / 10; + } + return chineseStr.toString(); + } + + /** + * 把中文转换为数字 如 二百二十 220
+ *

    + *
  • 一百一十二 -》 112
  • + *
  • 一千零一十二 -》 1012
  • + *
+ * + * @param chinese 中文字符 + * @return 数字 + * @since 5.6.0 + */ + public static int chineseToNumber(String chinese) { + final int length = chinese.length(); + int result = 0; + + // 节总和 + int section = 0; + int number = 0; + ChineseUnit unit = null; + char c; + for (int i = 0; i < length; i++) { + c = chinese.charAt(i); + final int num = chineseToNumber(c); + if (num >= 0) { + if (num == 0) { + // 遇到零时节结束,权位失效,比如两万二零一十 + if (number > 0 && null != unit) { + section += number * (unit.value / 10); + } + unit = null; + } else if (number > 0) { + // 多个数字同时出现,报错 + throw new IllegalArgumentException(StrUtil.format("Bad number '{}{}' at: {}", chinese.charAt(i - 1), c, i)); + } + // 普通数字 + number = num; + } else { + unit = chineseToUnit(c); + if (null == unit) { + // 出现非法字符 + throw new IllegalArgumentException(StrUtil.format("Unknown unit '{}' at: {}", c, i)); + } + + //单位 + if (unit.secUnit) { + // 节单位,按照节求和 + section = (section + number) * unit.value; + result += section; + section = 0; + } else { + // 非节单位,和单位前的单数字组合为值 + int unitNumber = number; + if (0 == number && 0 == i) { + // issue#1726,对于单位开头的数组,默认赋予1 + // 十二 -> 一十二 + // 百二 -> 一百二 + unitNumber = 1; + } + section += (unitNumber * unit.value); + } + number = 0; + } + } + + if (number > 0 && null != unit) { + number = number * (unit.value / 10); + } + + return result + section + number; + } + + /** + * 查找对应的权对象 + * + * @param chinese 中文权位名 + * @return 权对象 + */ + private static ChineseUnit chineseToUnit(char chinese) { + for (ChineseUnit chineseNameValue : CHINESE_NAME_VALUE) { + if (chineseNameValue.name == chinese) { + return chineseNameValue; + } + } + return null; + } + + /** + * 将汉字单个数字转换为int类型数字 + * + * @param chinese 汉字数字,支持简体和繁体 + * @return 数字,-1表示未找到 + * @since 5.6.4 + */ + private static int chineseToNumber(char chinese) { + if ('两' == chinese) { + // 口语纠正 + chinese = '二'; + } + final int i = ArrayUtil.indexOf(DIGITS, chinese); + if (i > 0) { + return (i + 1) / 2; + } + return i; + } + + /** + * 单个数字转汉字 + * + * @param number 数字 + * @param isUseTraditional 是否使用繁体 + * @return 汉字 + */ + private static char numberToChinese(int number, boolean isUseTraditional) { + if (0 == number) { + return DIGITS[0]; + } + return DIGITS[number * 2 - (isUseTraditional ? 0 : 1)]; + } + + /** + * 获取对应级别的单位 + * + * @param index 级别,0表示各位,1表示十位,2表示百位,以此类推 + * @param isUseTraditional 是否使用繁体 + * @return 单位 + */ + private static String getUnitName(int index, boolean isUseTraditional) { + if (0 == index) { + return StrUtil.EMPTY; + } + return String.valueOf(CHINESE_NAME_VALUE[index * 2 - (isUseTraditional ? 0 : 1)].name); + } + + /** + * 权位 + * + * @author totalo + * @since 5.6.0 + */ + private static class ChineseUnit { + /** + * 中文权名称 + */ + private final char name; + /** + * 10的倍数值 + */ + private final int value; + /** + * 是否为节权位,它不是与之相邻的数字的倍数,而是整个小节的倍数。
+ * 例如二十三万,万是节权位,与三无关,而和二十三关联 + */ + private final boolean secUnit; + + /** + * 构造 + * + * @param name 名称 + * @param value 值,即10的倍数 + * @param secUnit 是否为节权位 + */ + public ChineseUnit(char name, int value, boolean secUnit) { + this.name = name; + this.value = value; + this.secUnit = secUnit; + } + } + + private static void addPreZero(StringBuilder chineseStr) { + if (StrUtil.isEmpty(chineseStr)) { + return; + } + final char c = chineseStr.charAt(0); + if ('零' != c) { + chineseStr.insert(0, '零'); + } + } +} diff --git a/src/main/java/cn/hutool/core/convert/NumberWithFormat.java b/src/main/java/cn/hutool/core/convert/NumberWithFormat.java new file mode 100644 index 0000000..6828049 --- /dev/null +++ b/src/main/java/cn/hutool/core/convert/NumberWithFormat.java @@ -0,0 +1,78 @@ +package cn.hutool.core.convert; + +import cn.hutool.core.convert.impl.DateConverter; +import cn.hutool.core.convert.impl.TemporalAccessorConverter; + +import java.lang.reflect.Type; +import java.time.temporal.TemporalAccessor; +import java.util.Date; + +/** + * 包含格式的数字转换器,主要针对带格式的时间戳 + * + * @author looly + * @since 5.8.13 + */ +public class NumberWithFormat extends Number implements TypeConverter{ + private static final long serialVersionUID = 1L; + + private final Number number; + private final String format; + + /** + * 构造 + * @param number 数字 + * @param format 格式 + */ + public NumberWithFormat(final Number number, final String format) { + this.number = number; + this.format = format; + } + + @SuppressWarnings("unchecked") + @Override + public Object convert(Type targetType, Object value) { + // 自定义日期格式 + if (null != this.format && targetType instanceof Class) { + final Class clazz = (Class) targetType; + // https://gitee.com/dromara/hutool/issues/I6IS5B + if (Date.class.isAssignableFrom(clazz)) { + return new DateConverter((Class) clazz, format).convert(this.number, null); + } else if (TemporalAccessor.class.isAssignableFrom(clazz)) { + return new TemporalAccessorConverter(clazz, format).convert(this.number, null); + } else if(String.class == clazz){ + return toString(); + } + + // 其他情况按照正常数字转换 + } + + // 按照正常数字转换 + return Convert.convertWithCheck(targetType, this.number, null, false); + } + + @Override + public int intValue() { + return this.number.intValue(); + } + + @Override + public long longValue() { + return this.number.longValue(); + } + + @Override + public float floatValue() { + return this.number.floatValue(); + } + + @Override + public double doubleValue() { + return this.number.doubleValue(); + } + + @Override + public String toString() { + return this.number.toString(); + } +} diff --git a/src/main/java/cn/hutool/core/convert/NumberWordFormatter.java b/src/main/java/cn/hutool/core/convert/NumberWordFormatter.java new file mode 100644 index 0000000..b78e6d2 --- /dev/null +++ b/src/main/java/cn/hutool/core/convert/NumberWordFormatter.java @@ -0,0 +1,182 @@ +package cn.hutool.core.convert; + +import cn.hutool.core.util.NumberUtil; +import cn.hutool.core.util.StrUtil; + +/** + * 将浮点数类型的number转换成英语的表达方式
+ * 参考博客:http://blog.csdn.net/eric_sunah/article/details/8713226 + * + * @author Looly,totalo + * @since 3.0.9 + */ +public class NumberWordFormatter { + + private static final String[] NUMBER = new String[]{"", "ONE", "TWO", "THREE", "FOUR", "FIVE", "SIX", "SEVEN", + "EIGHT", "NINE"}; + private static final String[] NUMBER_TEEN = new String[]{"TEN", "ELEVEN", "TWELVE", "THIRTEEN", "FOURTEEN", + "FIFTEEN", "SIXTEEN", "SEVENTEEN", "EIGHTEEN", "NINETEEN"}; + private static final String[] NUMBER_TEN = new String[]{"TEN", "TWENTY", "THIRTY", "FORTY", "FIFTY", "SIXTY", + "SEVENTY", "EIGHTY", "NINETY"}; + private static final String[] NUMBER_MORE = new String[]{"", "THOUSAND", "MILLION", "BILLION"}; + + private static final String[] NUMBER_SUFFIX = new String[]{"k", "w", "", "m", "", "", "b", "", "", "t", "", "", "p", "", "", "e"}; + + /** + * 将阿拉伯数字转为英文表达式 + * + * @param x 阿拉伯数字,可以为{@link Number}对象,也可以是普通对象,最后会使用字符串方式处理 + * @return 英文表达式 + */ + public static String format(Object x) { + if (x != null) { + return format(x.toString()); + } else { + return StrUtil.EMPTY; + } + } + + /** + * 将阿拉伯数字转化为简洁计数单位,例如 2100 =》 2.1k + * 范围默认只到w + * + * @param value 被格式化的数字 + * @return 格式化后的数字 + * @since 5.5.9 + */ + public static String formatSimple(long value) { + return formatSimple(value, true); + } + + /** + * 将阿拉伯数字转化为简介计数单位,例如 2100 =》 2.1k + * + * @param value 对应数字的值 + * @param isTwo 控制是否为只为k、w,例如当为{@code false}时返回4.38m,{@code true}返回438.43w + * @return 格式化后的数字 + * @since 5.5.9 + */ + public static String formatSimple(long value, boolean isTwo) { + if (value < 1000) { + return String.valueOf(value); + } + int index = -1; + double res = value; + while (res > 10 && (!isTwo || index < 1)) { + if (res >= 1000) { + res = res / 1000; + index++; + } + if (res > 10) { + res = res / 10; + index++; + } + } + return String.format("%s%s", NumberUtil.decimalFormat("#.##", res), NUMBER_SUFFIX[index]); + } + + /** + * 将阿拉伯数字转为英文表达式 + * + * @param x 阿拉伯数字字符串 + * @return 英文表达式 + */ + private static String format(String x) { + int z = x.indexOf("."); // 取小数点位置 + String lstr, rstr = ""; + if (z > -1) { // 看是否有小数,如果有,则分别取左边和右边 + lstr = x.substring(0, z); + rstr = x.substring(z + 1); + } else { + // 否则就是全部 + lstr = x; + } + + String lstrrev = StrUtil.reverse(lstr); // 对左边的字串取反 + String[] a = new String[5]; // 定义5个字串变量来存放解析出来的叁位一组的字串 + + switch (lstrrev.length() % 3) { + case 1: + lstrrev += "00"; + break; + case 2: + lstrrev += "0"; + break; + } + StringBuilder lm = new StringBuilder(); // 用来存放转换后的整数部分 + for (int i = 0; i < lstrrev.length() / 3; i++) { + a[i] = StrUtil.reverse(lstrrev.substring(3 * i, 3 * i + 3)); // 截取第一个三位 + if (!"000".equals(a[i])) { // 用来避免这种情况:1000000 = one million + // thousand only + if (i != 0) { + lm.insert(0, transThree(a[i]) + " " + parseMore(i) + " "); // 加: + // thousand、million、billion + } else { + // 防止i=0时, 在多加两个空格. + lm = new StringBuilder(transThree(a[i])); + } + } else { + lm.append(transThree(a[i])); + } + } + + String xs = ""; // 用来存放转换后小数部分 + if (z > -1) { + xs = "AND CENTS " + transTwo(rstr) + " "; // 小数部分存在时转换小数 + } + + return lm.toString().trim() + " " + xs + "ONLY"; + } + + private static String parseFirst(String s) { + return NUMBER[Integer.parseInt(s.substring(s.length() - 1))]; + } + + private static String parseTeen(String s) { + return NUMBER_TEEN[Integer.parseInt(s) - 10]; + } + + private static String parseTen(String s) { + return NUMBER_TEN[Integer.parseInt(s.substring(0, 1)) - 1]; + } + + private static String parseMore(int i) { + return NUMBER_MORE[i]; + } + + // 两位 + private static String transTwo(String s) { + String value; + // 判断位数 + if (s.length() > 2) { + s = s.substring(0, 2); + } else if (s.length() < 2) { + s = "0" + s; + } + + if (s.startsWith("0")) {// 07 - seven 是否小於10 + value = parseFirst(s); + } else if (s.startsWith("1")) {// 17 seventeen 是否在10和20之间 + value = parseTeen(s); + } else if (s.endsWith("0")) {// 是否在10与100之间的能被10整除的数 + value = parseTen(s); + } else { + value = parseTen(s) + " " + parseFirst(s); + } + return value; + } + + // 制作叁位的数 + // s.length = 3 + private static String transThree(String s) { + String value; + if (s.startsWith("0")) {// 是否小於100 + value = transTwo(s.substring(1)); + } else if ("00".equals(s.substring(1))) {// 是否被100整除 + value = parseFirst(s.substring(0, 1)) + " HUNDRED"; + } else { + value = parseFirst(s.substring(0, 1)) + " HUNDRED AND " + transTwo(s.substring(1)); + } + return value; + } +} diff --git a/src/main/java/cn/hutool/core/convert/TypeConverter.java b/src/main/java/cn/hutool/core/convert/TypeConverter.java new file mode 100644 index 0000000..b72acd7 --- /dev/null +++ b/src/main/java/cn/hutool/core/convert/TypeConverter.java @@ -0,0 +1,24 @@ +package cn.hutool.core.convert; + +import java.lang.reflect.Type; + +/** + * 类型转换接口函数,根据给定的值和目标类型,由用户自定义转换规则。 + * + * @author looly + * @since 5.8.0 + */ +@FunctionalInterface +public interface TypeConverter { + + /** + * 转换为指定类型
+ * 如果类型无法确定,将读取默认值的类型做为目标类型 + * + * @param targetType 目标Type,非泛型类使用 + * @param value 原始值 + * @return 转换后的值 + * @throws IllegalArgumentException 无法确定目标类型,且默认值为{@code null},无法确定类型 + */ + Object convert(Type targetType, Object value); +} diff --git a/src/main/java/cn/hutool/core/convert/impl/ArrayConverter.java b/src/main/java/cn/hutool/core/convert/impl/ArrayConverter.java new file mode 100644 index 0000000..99cf27b --- /dev/null +++ b/src/main/java/cn/hutool/core/convert/impl/ArrayConverter.java @@ -0,0 +1,212 @@ +package cn.hutool.core.convert.impl; + +import cn.hutool.core.codec.Base64; +import cn.hutool.core.collection.IterUtil; +import cn.hutool.core.convert.AbstractConverter; +import cn.hutool.core.convert.Convert; +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.ByteUtil; +import cn.hutool.core.util.CharUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; + +import java.io.Serializable; +import java.lang.reflect.Array; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; + +/** + * 数组转换器,包括原始类型数组 + * + * @author Looly + */ +public class ArrayConverter extends AbstractConverter { + private static final long serialVersionUID = 1L; + + private final Class targetType; + /** + * 目标元素类型 + */ + private final Class targetComponentType; + + /** + * 是否忽略元素转换错误 + */ + private boolean ignoreElementError; + + /** + * 构造 + * + * @param targetType 目标数组类型 + */ + public ArrayConverter(Class targetType) { + this(targetType, false); + } + + /** + * 构造 + * + * @param targetType 目标数组类型 + * @param ignoreElementError 是否忽略元素转换错误 + */ + public ArrayConverter(Class targetType, boolean ignoreElementError) { + if (null == targetType) { + // 默认Object数组 + targetType = Object[].class; + } + + if (targetType.isArray()) { + this.targetType = targetType; + this.targetComponentType = targetType.getComponentType(); + } else { + //用户传入类为非数组时,按照数组元素类型对待 + this.targetComponentType = targetType; + this.targetType = ArrayUtil.getArrayType(targetType); + } + + this.ignoreElementError = ignoreElementError; + } + + @Override + protected Object convertInternal(Object value) { + return value.getClass().isArray() ? convertArrayToArray(value) : convertObjectToArray(value); + } + + @SuppressWarnings({"unchecked", "rawtypes"}) + @Override + public Class getTargetType() { + return this.targetType; + } + + /** + * 设置是否忽略元素转换错误 + * + * @param ignoreElementError 是否忽略元素转换错误 + * @since 5.4.3 + */ + public void setIgnoreElementError(boolean ignoreElementError) { + this.ignoreElementError = ignoreElementError; + } + + // -------------------------------------------------------------------------------------- Private method start + + /** + * 数组对数组转换 + * + * @param array 被转换的数组值 + * @return 转换后的数组 + */ + private Object convertArrayToArray(Object array) { + final Class valueComponentType = ArrayUtil.getComponentType(array); + + if (valueComponentType == targetComponentType) { + return array; + } + + final int len = ArrayUtil.length(array); + final Object result = Array.newInstance(targetComponentType, len); + + for (int i = 0; i < len; i++) { + Array.set(result, i, convertComponentType(Array.get(array, i))); + } + return result; + } + + /** + * 非数组对数组转换 + * + * @param value 被转换值 + * @return 转换后的数组 + */ + private Object convertObjectToArray(Object value) { + if (value instanceof CharSequence) { + if (targetComponentType == char.class || targetComponentType == Character.class) { + return convertArrayToArray(value.toString().toCharArray()); + } + + //issue#2365 + // 字符串转bytes,首先判断是否为Base64,是则转换,否则按照默认getBytes方法。 + if(targetComponentType == byte.class){ + final String str = value.toString(); + if(Base64.isBase64(str)){ + return Base64.decode(value.toString()); + } + return str.getBytes(); + } + + // 单纯字符串情况下按照逗号分隔后劈开 + final String[] strings = StrUtil.splitToArray(value.toString(), CharUtil.COMMA); + return convertArrayToArray(strings); + } + + Object result; + if (value instanceof List) { + // List转数组 + final List list = (List) value; + result = Array.newInstance(targetComponentType, list.size()); + for (int i = 0; i < list.size(); i++) { + Array.set(result, i, convertComponentType(list.get(i))); + } + } else if (value instanceof Collection) { + // 集合转数组 + final Collection collection = (Collection) value; + result = Array.newInstance(targetComponentType, collection.size()); + + int i = 0; + for (Object element : collection) { + Array.set(result, i, convertComponentType(element)); + i++; + } + } else if (value instanceof Iterable) { + // 可循环对象转数组,可循环对象无法获取长度,因此先转为List后转为数组 + final List list = IterUtil.toList((Iterable) value); + result = Array.newInstance(targetComponentType, list.size()); + for (int i = 0; i < list.size(); i++) { + Array.set(result, i, convertComponentType(list.get(i))); + } + } else if (value instanceof Iterator) { + // 可循环对象转数组,可循环对象无法获取长度,因此先转为List后转为数组 + final List list = IterUtil.toList((Iterator) value); + result = Array.newInstance(targetComponentType, list.size()); + for (int i = 0; i < list.size(); i++) { + Array.set(result, i, convertComponentType(list.get(i))); + } + }else if (value instanceof Number && byte.class == targetComponentType) { + // 用户可能想序列化指定对象 + result = ByteUtil.numberToBytes((Number)value); + } else if (value instanceof Serializable && byte.class == targetComponentType) { + // 用户可能想序列化指定对象 + result = ObjectUtil.serialize(value); + } else { + // everything else: + result = convertToSingleElementArray(value); + } + + return result; + } + + /** + * 单元素数组 + * + * @param value 被转换的值 + * @return 数组,只包含一个元素 + */ + private Object[] convertToSingleElementArray(Object value) { + final Object[] singleElementArray = ArrayUtil.newArray(targetComponentType, 1); + singleElementArray[0] = convertComponentType(value); + return singleElementArray; + } + + /** + * 转换元素类型 + * + * @param value 值 + * @return 转换后的值,转换失败若{@link #ignoreElementError}为true,返回null,否则抛出异常 + * @since 5.4.3 + */ + private Object convertComponentType(Object value) { + return Convert.convertWithCheck(this.targetComponentType, value, null, this.ignoreElementError); + } + // -------------------------------------------------------------------------------------- Private method end +} diff --git a/src/main/java/cn/hutool/core/convert/impl/AtomicBooleanConverter.java b/src/main/java/cn/hutool/core/convert/impl/AtomicBooleanConverter.java new file mode 100644 index 0000000..0da6970 --- /dev/null +++ b/src/main/java/cn/hutool/core/convert/impl/AtomicBooleanConverter.java @@ -0,0 +1,26 @@ +package cn.hutool.core.convert.impl; + +import java.util.concurrent.atomic.AtomicBoolean; + +import cn.hutool.core.convert.AbstractConverter; +import cn.hutool.core.util.BooleanUtil; + +/** + * {@link AtomicBoolean}转换器 + * + * @author Looly + * @since 3.0.8 + */ +public class AtomicBooleanConverter extends AbstractConverter { + private static final long serialVersionUID = 1L; + + @Override + protected AtomicBoolean convertInternal(Object value) { + if (value instanceof Boolean) { + return new AtomicBoolean((Boolean) value); + } + final String valueStr = convertToStr(value); + return new AtomicBoolean(BooleanUtil.toBoolean(valueStr)); + } + +} diff --git a/src/main/java/cn/hutool/core/convert/impl/AtomicIntegerArrayConverter.java b/src/main/java/cn/hutool/core/convert/impl/AtomicIntegerArrayConverter.java new file mode 100644 index 0000000..0474351 --- /dev/null +++ b/src/main/java/cn/hutool/core/convert/impl/AtomicIntegerArrayConverter.java @@ -0,0 +1,22 @@ +package cn.hutool.core.convert.impl; + +import cn.hutool.core.convert.AbstractConverter; +import cn.hutool.core.convert.Convert; + +import java.util.concurrent.atomic.AtomicIntegerArray; + +/** + * {@link AtomicIntegerArray}转换器 + * + * @author Looly + * @since 5.4.5 + */ +public class AtomicIntegerArrayConverter extends AbstractConverter { + private static final long serialVersionUID = 1L; + + @Override + protected AtomicIntegerArray convertInternal(Object value) { + return new AtomicIntegerArray(Convert.convert(int[].class, value)); + } + +} diff --git a/src/main/java/cn/hutool/core/convert/impl/AtomicLongArrayConverter.java b/src/main/java/cn/hutool/core/convert/impl/AtomicLongArrayConverter.java new file mode 100644 index 0000000..3072075 --- /dev/null +++ b/src/main/java/cn/hutool/core/convert/impl/AtomicLongArrayConverter.java @@ -0,0 +1,22 @@ +package cn.hutool.core.convert.impl; + +import cn.hutool.core.convert.AbstractConverter; +import cn.hutool.core.convert.Convert; + +import java.util.concurrent.atomic.AtomicLongArray; + +/** + * {@link AtomicLongArray}转换器 + * + * @author Looly + * @since 5.4.5 + */ +public class AtomicLongArrayConverter extends AbstractConverter { + private static final long serialVersionUID = 1L; + + @Override + protected AtomicLongArray convertInternal(Object value) { + return new AtomicLongArray(Convert.convert(long[].class, value)); + } + +} diff --git a/src/main/java/cn/hutool/core/convert/impl/AtomicReferenceConverter.java b/src/main/java/cn/hutool/core/convert/impl/AtomicReferenceConverter.java new file mode 100644 index 0000000..3df94e8 --- /dev/null +++ b/src/main/java/cn/hutool/core/convert/impl/AtomicReferenceConverter.java @@ -0,0 +1,36 @@ +package cn.hutool.core.convert.impl; + +import java.lang.reflect.Type; +import java.util.concurrent.atomic.AtomicReference; + +import cn.hutool.core.convert.AbstractConverter; +import cn.hutool.core.convert.ConverterRegistry; +import cn.hutool.core.util.TypeUtil; + +/** + * {@link AtomicReference}转换器 + * + * @author Looly + * @since 3.0.8 + */ +@SuppressWarnings("rawtypes") +public class AtomicReferenceConverter extends AbstractConverter { + private static final long serialVersionUID = 1L; + + @Override + protected AtomicReference convertInternal(Object value) { + + //尝试将值转换为Reference泛型的类型 + Object targetValue = null; + final Type paramType = TypeUtil.getTypeArgument(AtomicReference.class); + if(!TypeUtil.isUnknown(paramType)){ + targetValue = ConverterRegistry.getInstance().convert(paramType, value); + } + if(null == targetValue){ + targetValue = value; + } + + return new AtomicReference<>(targetValue); + } + +} diff --git a/src/main/java/cn/hutool/core/convert/impl/BeanConverter.java b/src/main/java/cn/hutool/core/convert/impl/BeanConverter.java new file mode 100644 index 0000000..9a7c238 --- /dev/null +++ b/src/main/java/cn/hutool/core/convert/impl/BeanConverter.java @@ -0,0 +1,91 @@ +package cn.hutool.core.convert.impl; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.bean.copier.BeanCopier; +import cn.hutool.core.bean.copier.CopyOptions; +import cn.hutool.core.bean.copier.ValueProvider; +import cn.hutool.core.convert.AbstractConverter; +import cn.hutool.core.convert.ConvertException; +import cn.hutool.core.map.MapProxy; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.ReflectUtil; +import cn.hutool.core.util.TypeUtil; + +import java.lang.reflect.Type; +import java.util.Map; + +/** + * Bean转换器,支持: + *
+ * Map =》 Bean
+ * Bean =》 Bean
+ * ValueProvider =》 Bean
+ * 
+ * + * @param Bean类型 + * @author Looly + * @since 4.0.2 + */ +public class BeanConverter extends AbstractConverter { + private static final long serialVersionUID = 1L; + + private final Type beanType; + private final Class beanClass; + private final CopyOptions copyOptions; + + /** + * 构造,默认转换选项,注入失败的字段忽略 + * + * @param beanType 转换成的目标Bean类型 + */ + public BeanConverter(Type beanType) { + this(beanType, CopyOptions.create().setIgnoreError(true)); + } + + /** + * 构造,默认转换选项,注入失败的字段忽略 + * + * @param beanClass 转换成的目标Bean类 + */ + public BeanConverter(Class beanClass) { + this(beanClass, CopyOptions.create().setIgnoreError(true)); + } + + /** + * 构造 + * + * @param beanType 转换成的目标Bean类 + * @param copyOptions Bean转换选项参数 + */ + @SuppressWarnings("unchecked") + public BeanConverter(Type beanType, CopyOptions copyOptions) { + this.beanType = beanType; + this.beanClass = (Class) TypeUtil.getClass(beanType); + this.copyOptions = copyOptions; + } + + @Override + protected T convertInternal(Object value) { + if(value instanceof Map || + value instanceof ValueProvider || + BeanUtil.isBean(value.getClass())) { + if(value instanceof Map && this.beanClass.isInterface()) { + // 将Map动态代理为Bean + return MapProxy.create((Map)value).toProxyBean(this.beanClass); + } + + //限定被转换对象类型 + return BeanCopier.create(value, ReflectUtil.newInstanceIfPossible(this.beanClass), this.beanType, this.copyOptions).copy(); + } else if(value instanceof byte[]){ + // 尝试反序列化 + return ObjectUtil.deserialize((byte[])value); + } + + throw new ConvertException("Unsupported source type: {}", value.getClass()); + } + + @Override + public Class getTargetType() { + return this.beanClass; + } +} diff --git a/src/main/java/cn/hutool/core/convert/impl/BooleanConverter.java b/src/main/java/cn/hutool/core/convert/impl/BooleanConverter.java new file mode 100644 index 0000000..caec14b --- /dev/null +++ b/src/main/java/cn/hutool/core/convert/impl/BooleanConverter.java @@ -0,0 +1,32 @@ +package cn.hutool.core.convert.impl; + +import cn.hutool.core.convert.AbstractConverter; +import cn.hutool.core.util.BooleanUtil; + +/** + * 布尔转换器 + * + *

+ * 对象转为boolean,规则如下: + *

+ *
+ *     1、数字0为false,其它数字为true
+ *     2、转换为字符串,形如"true", "yes", "y", "t", "ok", "1", "on", "是", "对", "真", "對", "√"为true,其它字符串为false.
+ * 
+ * + * @author Looly + */ +public class BooleanConverter extends AbstractConverter { + private static final long serialVersionUID = 1L; + + @Override + protected Boolean convertInternal(Object value) { + if (value instanceof Number) { + // 0为false,其它数字为true + return 0 != ((Number) value).doubleValue(); + } + //Object不可能出现Primitive类型,故忽略 + return BooleanUtil.toBoolean(convertToStr(value)); + } + +} diff --git a/src/main/java/cn/hutool/core/convert/impl/CalendarConverter.java b/src/main/java/cn/hutool/core/convert/impl/CalendarConverter.java new file mode 100644 index 0000000..e6c55e2 --- /dev/null +++ b/src/main/java/cn/hutool/core/convert/impl/CalendarConverter.java @@ -0,0 +1,57 @@ +package cn.hutool.core.convert.impl; + +import java.util.Calendar; +import java.util.Date; + +import cn.hutool.core.convert.AbstractConverter; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.util.StrUtil; + +/** + * 日期转换器 + * + * @author Looly + * + */ +public class CalendarConverter extends AbstractConverter { + private static final long serialVersionUID = 1L; + + /** 日期格式化 */ + private String format; + + /** + * 获取日期格式 + * + * @return 设置日期格式 + */ + public String getFormat() { + return format; + } + + /** + * 设置日期格式 + * + * @param format 日期格式 + */ + public void setFormat(String format) { + this.format = format; + } + + @Override + protected Calendar convertInternal(Object value) { + // Handle Date + if (value instanceof Date) { + return DateUtil.calendar((Date)value); + } + + // Handle Long + if (value instanceof Long) { + //此处使用自动拆装箱 + return DateUtil.calendar((Long)value); + } + + final String valueStr = convertToStr(value); + return DateUtil.calendar(StrUtil.isBlank(format) ? DateUtil.parse(valueStr) : DateUtil.parse(valueStr, format)); + } + +} diff --git a/src/main/java/cn/hutool/core/convert/impl/CastConverter.java b/src/main/java/cn/hutool/core/convert/impl/CastConverter.java new file mode 100644 index 0000000..7eb5fc6 --- /dev/null +++ b/src/main/java/cn/hutool/core/convert/impl/CastConverter.java @@ -0,0 +1,28 @@ +package cn.hutool.core.convert.impl; + +import cn.hutool.core.convert.AbstractConverter; +import cn.hutool.core.convert.ConvertException; + +/** + * 强转转换器 + * + * @author Looly + * @param 强制转换到的类型 + * @since 4.0.2 + */ +public class CastConverter extends AbstractConverter { + private static final long serialVersionUID = 1L; + + private Class targetType; + + @Override + protected T convertInternal(Object value) { + // 由于在AbstractConverter中已经有类型判断并强制转换,因此当在上一步强制转换失败时直接抛出异常 + throw new ConvertException("Can not cast value to [{}]", this.targetType); + } + + @Override + public Class getTargetType() { + return this.targetType; + } +} diff --git a/src/main/java/cn/hutool/core/convert/impl/CharacterConverter.java b/src/main/java/cn/hutool/core/convert/impl/CharacterConverter.java new file mode 100644 index 0000000..85b8e02 --- /dev/null +++ b/src/main/java/cn/hutool/core/convert/impl/CharacterConverter.java @@ -0,0 +1,29 @@ +package cn.hutool.core.convert.impl; + +import cn.hutool.core.convert.AbstractConverter; +import cn.hutool.core.util.BooleanUtil; +import cn.hutool.core.util.StrUtil; + +/** + * 字符转换器 + * + * @author Looly + * + */ +public class CharacterConverter extends AbstractConverter { + private static final long serialVersionUID = 1L; + + @Override + protected Character convertInternal(Object value) { + if (value instanceof Boolean) { + return BooleanUtil.toCharacter((Boolean) value); + } else { + final String valueStr = convertToStr(value); + if (StrUtil.isNotBlank(valueStr)) { + return valueStr.charAt(0); + } + } + return null; + } + +} diff --git a/src/main/java/cn/hutool/core/convert/impl/CharsetConverter.java b/src/main/java/cn/hutool/core/convert/impl/CharsetConverter.java new file mode 100644 index 0000000..f1fd5e0 --- /dev/null +++ b/src/main/java/cn/hutool/core/convert/impl/CharsetConverter.java @@ -0,0 +1,21 @@ +package cn.hutool.core.convert.impl; + +import java.nio.charset.Charset; + +import cn.hutool.core.convert.AbstractConverter; +import cn.hutool.core.util.CharsetUtil; + +/** + * 编码对象转换器 + * @author Looly + * + */ +public class CharsetConverter extends AbstractConverter{ + private static final long serialVersionUID = 1L; + + @Override + protected Charset convertInternal(Object value) { + return CharsetUtil.charset(convertToStr(value)); + } + +} diff --git a/src/main/java/cn/hutool/core/convert/impl/ClassConverter.java b/src/main/java/cn/hutool/core/convert/impl/ClassConverter.java new file mode 100644 index 0000000..bbdb61f --- /dev/null +++ b/src/main/java/cn/hutool/core/convert/impl/ClassConverter.java @@ -0,0 +1,39 @@ +package cn.hutool.core.convert.impl; + +import cn.hutool.core.convert.AbstractConverter; +import cn.hutool.core.util.ClassLoaderUtil; + +/** + * 类转换器
+ * 将类名转换为类,默认初始化这个类(执行static块) + * + * @author Looly + */ +public class ClassConverter extends AbstractConverter> { + private static final long serialVersionUID = 1L; + + private final boolean isInitialized; + + /** + * 构造 + */ + public ClassConverter() { + this(true); + } + + /** + * 构造 + * + * @param isInitialized 是否初始化类(调用static模块内容和初始化static属性) + * @since 5.5.0 + */ + public ClassConverter(boolean isInitialized) { + this.isInitialized = isInitialized; + } + + @Override + protected Class convertInternal(Object value) { + return ClassLoaderUtil.loadClass(convertToStr(value), isInitialized); + } + +} diff --git a/src/main/java/cn/hutool/core/convert/impl/CollectionConverter.java b/src/main/java/cn/hutool/core/convert/impl/CollectionConverter.java new file mode 100644 index 0000000..2fb1c8b --- /dev/null +++ b/src/main/java/cn/hutool/core/convert/impl/CollectionConverter.java @@ -0,0 +1,78 @@ +package cn.hutool.core.convert.impl; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.convert.Converter; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.TypeUtil; + +import java.lang.reflect.Type; +import java.util.Collection; + +/** + * 各种集合类转换器 + * + * @author Looly + * @since 3.0.8 + */ +public class CollectionConverter implements Converter> { + + /** 集合类型 */ + private final Type collectionType; + /** 集合元素类型 */ + private final Type elementType; + + /** + * 构造,默认集合类型使用{@link Collection} + */ + public CollectionConverter() { + this(Collection.class); + } + + // ---------------------------------------------------------------------------------------------- Constractor start + /** + * 构造 + * + * @param collectionType 集合类型 + */ + public CollectionConverter(Type collectionType) { + this(collectionType, TypeUtil.getTypeArgument(collectionType)); + } + + /** + * 构造 + * + * @param collectionType 集合类型 + */ + public CollectionConverter(Class collectionType) { + this(collectionType, TypeUtil.getTypeArgument(collectionType)); + } + + /** + * 构造 + * + * @param collectionType 集合类型 + * @param elementType 集合元素类型 + */ + public CollectionConverter(Type collectionType, Type elementType) { + this.collectionType = collectionType; + this.elementType = elementType; + } + // ---------------------------------------------------------------------------------------------- Constractor end + + @Override + public Collection convert(Object value, Collection defaultValue) throws IllegalArgumentException { + final Collection result = convertInternal(value); + return ObjectUtil.defaultIfNull(result, defaultValue); + } + + /** + * 内部转换 + * + * @param value 值 + * @return 转换后的集合对象 + */ + protected Collection convertInternal(Object value) { + final Collection collection = CollUtil.create(TypeUtil.getClass(this.collectionType), TypeUtil.getClass(this.elementType)); + return CollUtil.addAll(collection, value, this.elementType); + } +} diff --git a/src/main/java/cn/hutool/core/convert/impl/CurrencyConverter.java b/src/main/java/cn/hutool/core/convert/impl/CurrencyConverter.java new file mode 100644 index 0000000..ffab3fb --- /dev/null +++ b/src/main/java/cn/hutool/core/convert/impl/CurrencyConverter.java @@ -0,0 +1,21 @@ +package cn.hutool.core.convert.impl; + +import java.util.Currency; + +import cn.hutool.core.convert.AbstractConverter; + +/** + * 货币{@link Currency} 转换器 + * + * @author Looly + * @since 3.0.8 + */ +public class CurrencyConverter extends AbstractConverter { + private static final long serialVersionUID = 1L; + + @Override + protected Currency convertInternal(Object value) { + return Currency.getInstance(convertToStr(value)); + } + +} diff --git a/src/main/java/cn/hutool/core/convert/impl/DateConverter.java b/src/main/java/cn/hutool/core/convert/impl/DateConverter.java new file mode 100644 index 0000000..1626e70 --- /dev/null +++ b/src/main/java/cn/hutool/core/convert/impl/DateConverter.java @@ -0,0 +1,136 @@ +package cn.hutool.core.convert.impl; + +import cn.hutool.core.convert.AbstractConverter; +import cn.hutool.core.convert.ConvertException; +import cn.hutool.core.date.DateTime; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.date.format.GlobalCustomFormat; +import cn.hutool.core.util.StrUtil; + +import java.time.temporal.TemporalAccessor; +import java.util.Calendar; + +/** + * 日期转换器 + * + * @author Looly + */ +public class DateConverter extends AbstractConverter { + private static final long serialVersionUID = 1L; + + private final Class targetType; + /** + * 日期格式化 + */ + private String format; + + /** + * 构造 + * + * @param targetType 目标类型 + */ + public DateConverter(Class targetType) { + this.targetType = targetType; + } + + /** + * 构造 + * + * @param targetType 目标类型 + * @param format 日期格式 + */ + public DateConverter(Class targetType, String format) { + this.targetType = targetType; + this.format = format; + } + + /** + * 获取日期格式 + * + * @return 设置日期格式 + */ + public String getFormat() { + return format; + } + + /** + * 设置日期格式 + * + * @param format 日期格式 + */ + public void setFormat(String format) { + this.format = format; + } + + @Override + protected java.util.Date convertInternal(Object value) { + if (value == null || (value instanceof CharSequence && StrUtil.isBlank(value.toString()))) { + return null; + } + if (value instanceof TemporalAccessor) { + return wrap(DateUtil.date((TemporalAccessor) value)); + } else if (value instanceof Calendar) { + return wrap(DateUtil.date((Calendar) value)); + } else if (value instanceof Number) { + return wrap(((Number) value).longValue()); + } else { + // 统一按照字符串处理 + final String valueStr = convertToStr(value); + final DateTime dateTime = StrUtil.isBlank(this.format) // + ? DateUtil.parse(valueStr) // + : DateUtil.parse(valueStr, this.format); + if (null != dateTime) { + return wrap(dateTime); + } + } + + throw new ConvertException("Can not convert {}:[{}] to {}", value.getClass().getName(), value, this.targetType.getName()); + } + + /** + * java.util.Date转为子类型 + * + * @param date Date + * @return 目标类型对象 + */ + private java.util.Date wrap(DateTime date) { + // 返回指定类型 + if (java.util.Date.class == targetType) { + return date.toJdkDate(); + } + if (DateTime.class == targetType) { + return date; + } + + throw new UnsupportedOperationException(StrUtil.format("Unsupported target Date type: {}", this.targetType.getName())); + } + + /** + * java.util.Date转为子类型 + * + * @param mills Date + * @return 目标类型对象 + */ + private java.util.Date wrap(long mills) { + if(GlobalCustomFormat.FORMAT_SECONDS.equals(this.format)){ + // Unix时间戳 + return DateUtil.date(mills * 1000); + } + + // 返回指定类型 + if (java.util.Date.class == targetType) { + return new java.util.Date(mills); + } + if (DateTime.class == targetType) { + return DateUtil.date(mills); + } + + throw new UnsupportedOperationException(StrUtil.format("Unsupported target Date type: {}", this.targetType.getName())); + } + + @SuppressWarnings("unchecked") + @Override + public Class getTargetType() { + return (Class) this.targetType; + } +} diff --git a/src/main/java/cn/hutool/core/convert/impl/DurationConverter.java b/src/main/java/cn/hutool/core/convert/impl/DurationConverter.java new file mode 100644 index 0000000..6f57532 --- /dev/null +++ b/src/main/java/cn/hutool/core/convert/impl/DurationConverter.java @@ -0,0 +1,29 @@ +package cn.hutool.core.convert.impl; + +import cn.hutool.core.convert.AbstractConverter; + +import java.time.Duration; +import java.time.temporal.TemporalAmount; + +/** + * + * {@link Duration}对象转换器 + * + * @author Looly + * @since 5.0.0 + */ +public class DurationConverter extends AbstractConverter { + private static final long serialVersionUID = 1L; + + @Override + protected Duration convertInternal(Object value) { + if(value instanceof TemporalAmount){ + return Duration.from((TemporalAmount) value); + } else if(value instanceof Long){ + return Duration.ofMillis((Long) value); + } else { + return Duration.parse(convertToStr(value)); + } + } + +} diff --git a/src/main/java/cn/hutool/core/convert/impl/EnumConverter.java b/src/main/java/cn/hutool/core/convert/impl/EnumConverter.java new file mode 100644 index 0000000..fdf4297 --- /dev/null +++ b/src/main/java/cn/hutool/core/convert/impl/EnumConverter.java @@ -0,0 +1,142 @@ +package cn.hutool.core.convert.impl; + +import cn.hutool.core.convert.AbstractConverter; +import cn.hutool.core.convert.ConvertException; +import cn.hutool.core.lang.EnumItem; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.map.WeakConcurrentMap; +import cn.hutool.core.util.ClassUtil; +import cn.hutool.core.util.EnumUtil; +import cn.hutool.core.util.ModifierUtil; +import cn.hutool.core.util.ReflectUtil; + +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * 无泛型检查的枚举转换器 + * + * @author Looly + * @since 4.0.2 + */ +@SuppressWarnings({"unchecked", "rawtypes"}) +public class EnumConverter extends AbstractConverter { + private static final long serialVersionUID = 1L; + + private static final WeakConcurrentMap, Map, Method>> VALUE_OF_METHOD_CACHE = new WeakConcurrentMap<>(); + + private final Class enumClass; + + /** + * 构造 + * + * @param enumClass 转换成的目标Enum类 + */ + public EnumConverter(Class enumClass) { + this.enumClass = enumClass; + } + + @Override + protected Object convertInternal(Object value) { + Enum enumValue = tryConvertEnum(value, this.enumClass); + if (null == enumValue && !(value instanceof String)) { + // 最后尝试先将value转String,再valueOf转换 + enumValue = Enum.valueOf(this.enumClass, convertToStr(value)); + } + + if (null != enumValue) { + return enumValue; + } + + throw new ConvertException("Can not convert {} to {}", value, this.enumClass); + } + + @Override + public Class getTargetType() { + return this.enumClass; + } + + /** + * 尝试转换,转换规则为: + *
    + *
  • 如果实现{@link EnumItem}接口,则调用fromInt或fromStr转换
  • + *
  • 找到类似转换的静态方法调用实现转换且优先使用
  • + *
  • 约定枚举类应该提供 valueOf(String) 和 valueOf(Integer)用于转换
  • + *
  • oriInt /name 转换托底
  • + *
+ * + * @param value 被转换的值 + * @param enumClass enum类 + * @return 对应的枚举值 + */ + protected static Enum tryConvertEnum(Object value, Class enumClass) { + if (value == null) { + return null; + } + + // EnumItem实现转换 + if (EnumItem.class.isAssignableFrom(enumClass)) { + final EnumItem first = (EnumItem) EnumUtil.getEnumAt(enumClass, 0); + if (null != first) { + if (value instanceof Integer) { + return (Enum) first.fromInt((Integer) value); + } else if (value instanceof String) { + return (Enum) first.fromStr(value.toString()); + } + } + } + + // 用户自定义方法 + // 查找枚举中所有返回值为目标枚举对象的方法,如果发现方法参数匹配,就执行之 + try { + final Map, Method> methodMap = getMethodMap(enumClass); + if (MapUtil.isNotEmpty(methodMap)) { + final Class valueClass = value.getClass(); + for (Map.Entry, Method> entry : methodMap.entrySet()) { + if (ClassUtil.isAssignable(entry.getKey(), valueClass)) { + return ReflectUtil.invokeStatic(entry.getValue(), value); + } + } + } + } catch (Exception ignore) { + //ignore + } + + //oriInt 应该滞后使用 以 GB/T 2261.1-2003 性别编码为例,对应整数并非连续数字会导致数字转枚举时失败 + //0 - 未知的性别 + //1 - 男性 + //2 - 女性 + //5 - 女性改(变)为男性 + //6 - 男性改(变)为女性 + //9 - 未说明的性别 + Enum enumResult = null; + if (value instanceof Integer) { + enumResult = EnumUtil.getEnumAt(enumClass, (Integer) value); + } else if (value instanceof String) { + try { + enumResult = Enum.valueOf(enumClass, (String) value); + } catch (IllegalArgumentException e) { + //ignore + } + } + + return enumResult; + } + + /** + * 获取用于转换为enum的所有static方法 + * + * @param enumClass 枚举类 + * @return 转换方法map,key为方法参数类型,value为方法 + */ + private static Map, Method> getMethodMap(Class enumClass) { + return VALUE_OF_METHOD_CACHE.computeIfAbsent(enumClass, (key) -> Arrays.stream(enumClass.getMethods()) + .filter(ModifierUtil::isStatic) + .filter(m -> m.getReturnType() == enumClass) + .filter(m -> m.getParameterCount() == 1) + .filter(m -> !"valueOf".equals(m.getName())) + .collect(Collectors.toMap(m -> m.getParameterTypes()[0], m -> m, (k1, k2) -> k1))); + } +} diff --git a/src/main/java/cn/hutool/core/convert/impl/LocaleConverter.java b/src/main/java/cn/hutool/core/convert/impl/LocaleConverter.java new file mode 100644 index 0000000..8f46f8d --- /dev/null +++ b/src/main/java/cn/hutool/core/convert/impl/LocaleConverter.java @@ -0,0 +1,41 @@ +package cn.hutool.core.convert.impl; + +import java.util.Locale; + +import cn.hutool.core.convert.AbstractConverter; +import cn.hutool.core.util.StrUtil; + +/** + * + * {@link Locale}对象转换器
+ * 只提供String转换支持 + * + * @author Looly + * @since 4.5.2 + */ +public class LocaleConverter extends AbstractConverter { + private static final long serialVersionUID = 1L; + + @Override + protected Locale convertInternal(Object value) { + try { + String str = convertToStr(value); + if (StrUtil.isEmpty(str)) { + return null; + } + + final String[] items = str.split("_"); + if (items.length == 1) { + return new Locale(items[0]); + } + if (items.length == 2) { + return new Locale(items[0], items[1]); + } + return new Locale(items[0], items[1], items[2]); + } catch (Exception e) { + // Ignore Exception + } + return null; + } + +} diff --git a/src/main/java/cn/hutool/core/convert/impl/MapConverter.java b/src/main/java/cn/hutool/core/convert/impl/MapConverter.java new file mode 100644 index 0000000..b99a5d6 --- /dev/null +++ b/src/main/java/cn/hutool/core/convert/impl/MapConverter.java @@ -0,0 +1,100 @@ +package cn.hutool.core.convert.impl; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.convert.AbstractConverter; +import cn.hutool.core.convert.ConverterRegistry; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.core.util.TypeUtil; + +import java.lang.reflect.Type; +import java.util.Map; +import java.util.Objects; + +/** + * {@link Map} 转换器 + * + * @author Looly + * @since 3.0.8 + */ +public class MapConverter extends AbstractConverter> { + private static final long serialVersionUID = 1L; + + /** Map类型 */ + private final Type mapType; + /** 键类型 */ + private final Type keyType; + /** 值类型 */ + private final Type valueType; + + /** + * 构造,Map的key和value泛型类型自动获取 + * + * @param mapType Map类型 + */ + public MapConverter(Type mapType) { + this(mapType, TypeUtil.getTypeArgument(mapType, 0), TypeUtil.getTypeArgument(mapType, 1)); + } + + /** + * 构造 + * + * @param mapType Map类型 + * @param keyType 键类型 + * @param valueType 值类型 + */ + public MapConverter(Type mapType, Type keyType, Type valueType) { + this.mapType = mapType; + this.keyType = keyType; + this.valueType = valueType; + } + + @Override + @SuppressWarnings({ "rawtypes", "unchecked" }) + protected Map convertInternal(Object value) { + Map map; + if (value instanceof Map) { + final Class valueClass = value.getClass(); + if(valueClass.equals(this.mapType)){ + final Type[] typeArguments = TypeUtil.getTypeArguments(valueClass); + if (null != typeArguments // + && 2 == typeArguments.length// + && Objects.equals(this.keyType, typeArguments[0]) // + && Objects.equals(this.valueType, typeArguments[1])) { + //对于键值对类型一致的Map对象,不再做转换,直接返回原对象 + return (Map) value; + } + } + map = MapUtil.createMap(TypeUtil.getClass(this.mapType)); + convertMapToMap((Map) value, map); + } else if (BeanUtil.isBean(value.getClass())) { + map = BeanUtil.beanToMap(value); + // 二次转换,转换键值类型 + map = convertInternal(map); + } else { + throw new UnsupportedOperationException(StrUtil.format("Unsupport toMap value type: {}", value.getClass().getName())); + } + return map; + } + + /** + * Map转Map + * + * @param srcMap 源Map + * @param targetMap 目标Map + */ + private void convertMapToMap(Map srcMap, Map targetMap) { + final ConverterRegistry convert = ConverterRegistry.getInstance(); + srcMap.forEach((key, value)->{ + key = TypeUtil.isUnknown(this.keyType) ? key : convert.convert(this.keyType, key); + value = TypeUtil.isUnknown(this.valueType) ? value : convert.convert(this.valueType, value); + targetMap.put(key, value); + }); + } + + @Override + @SuppressWarnings("unchecked") + public Class> getTargetType() { + return (Class>) TypeUtil.getClass(this.mapType); + } +} diff --git a/src/main/java/cn/hutool/core/convert/impl/NumberConverter.java b/src/main/java/cn/hutool/core/convert/impl/NumberConverter.java new file mode 100644 index 0000000..c6f23b0 --- /dev/null +++ b/src/main/java/cn/hutool/core/convert/impl/NumberConverter.java @@ -0,0 +1,261 @@ +package cn.hutool.core.convert.impl; + +import cn.hutool.core.convert.AbstractConverter; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.util.BooleanUtil; +import cn.hutool.core.util.ByteUtil; +import cn.hutool.core.util.NumberUtil; +import cn.hutool.core.util.StrUtil; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.time.temporal.TemporalAccessor; +import java.util.Calendar; +import java.util.Date; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.DoubleAdder; +import java.util.concurrent.atomic.LongAdder; +import java.util.function.Function; + +/** + * 数字转换器
+ * 支持类型为:
+ *
    + *
  • {@code java.lang.Byte}
  • + *
  • {@code java.lang.Short}
  • + *
  • {@code java.lang.Integer}
  • + *
  • {@code java.util.concurrent.atomic.AtomicInteger}
  • + *
  • {@code java.lang.Long}
  • + *
  • {@code java.util.concurrent.atomic.AtomicLong}
  • + *
  • {@code java.lang.Float}
  • + *
  • {@code java.lang.Double}
  • + *
  • {@code java.math.BigDecimal}
  • + *
  • {@code java.math.BigInteger}
  • + *
+ * + * @author Looly + */ +public class NumberConverter extends AbstractConverter { + private static final long serialVersionUID = 1L; + + private final Class targetType; + + /** + * 构造 + */ + public NumberConverter() { + this.targetType = Number.class; + } + + /** + * 构造
+ * + * @param clazz 需要转换的数字类型,默认 {@link Number} + */ + public NumberConverter(Class clazz) { + this.targetType = (null == clazz) ? Number.class : clazz; + } + + @Override + @SuppressWarnings("unchecked") + public Class getTargetType() { + return (Class) this.targetType; + } + + @Override + protected Number convertInternal(Object value) { + return convert(value, this.targetType, this::convertToStr); + } + + @Override + protected String convertToStr(Object value) { + final String result = StrUtil.trim(super.convertToStr(value)); + if (null != result && result.length() > 1) { + final char c = Character.toUpperCase(result.charAt(result.length() - 1)); + if (c == 'D' || c == 'L' || c == 'F') { + // 类型标识形式(例如123.6D) + return StrUtil.subPre(result, -1); + } + } + + return result; + } + + /** + * 转换对象为数字,支持的对象包括: + *
    + *
  • Number对象
  • + *
  • Boolean
  • + *
  • byte[]
  • + *
  • String
  • + *
+ * + * + * @param value 对象值 + * @param targetType 目标的数字类型 + * @param toStrFunc 转换为字符串的函数 + * @return 转换后的数字 + * @since 5.5.0 + */ + protected static Number convert(Object value, Class targetType, Function toStrFunc) { + // 枚举转换为数字默认为其顺序 + if (value instanceof Enum) { + return convert(((Enum) value).ordinal(), targetType, toStrFunc); + } + + // since 5.7.18 + if(value instanceof byte[]){ + return ByteUtil.bytesToNumber((byte[])value, targetType, ByteUtil.DEFAULT_ORDER); + } + + if (Byte.class == targetType) { + if (value instanceof Number) { + return ((Number) value).byteValue(); + } else if (value instanceof Boolean) { + return BooleanUtil.toByteObj((Boolean) value); + } + final String valueStr = toStrFunc.apply(value); + try{ + return StrUtil.isBlank(valueStr) ? null : Byte.valueOf(valueStr); + } catch (NumberFormatException e){ + return NumberUtil.parseNumber(valueStr).byteValue(); + } + } else if (Short.class == targetType) { + if (value instanceof Number) { + return ((Number) value).shortValue(); + } else if (value instanceof Boolean) { + return BooleanUtil.toShortObj((Boolean) value); + } + final String valueStr = toStrFunc.apply((value)); + try{ + return StrUtil.isBlank(valueStr) ? null : Short.valueOf(valueStr); + } catch (NumberFormatException e){ + return NumberUtil.parseNumber(valueStr).shortValue(); + } + } else if (Integer.class == targetType) { + if (value instanceof Number) { + return ((Number) value).intValue(); + } else if (value instanceof Boolean) { + return BooleanUtil.toInteger((Boolean) value); + } else if (value instanceof Date) { + return (int) ((Date) value).getTime(); + } else if (value instanceof Calendar) { + return (int) ((Calendar) value).getTimeInMillis(); + } else if (value instanceof TemporalAccessor) { + return (int) DateUtil.toInstant((TemporalAccessor) value).toEpochMilli(); + } + final String valueStr = toStrFunc.apply((value)); + return StrUtil.isBlank(valueStr) ? null : NumberUtil.parseInt(valueStr); + } else if (AtomicInteger.class == targetType) { + final Number number = convert(value, Integer.class, toStrFunc); + if (null != number) { + return new AtomicInteger(number.intValue()); + } + } else if (Long.class == targetType) { + if (value instanceof Number) { + return ((Number) value).longValue(); + } else if (value instanceof Boolean) { + return BooleanUtil.toLongObj((Boolean) value); + } else if (value instanceof Date) { + return ((Date) value).getTime(); + } else if (value instanceof Calendar) { + return ((Calendar) value).getTimeInMillis(); + } else if (value instanceof TemporalAccessor) { + return DateUtil.toInstant((TemporalAccessor) value).toEpochMilli(); + } + final String valueStr = toStrFunc.apply((value)); + return StrUtil.isBlank(valueStr) ? null : NumberUtil.parseLong(valueStr); + } else if (AtomicLong.class == targetType) { + final Number number = convert(value, Long.class, toStrFunc); + if (null != number) { + return new AtomicLong(number.longValue()); + } + } else if (LongAdder.class == targetType) { + //jdk8 新增 + final Number number = convert(value, Long.class, toStrFunc); + if (null != number) { + final LongAdder longValue = new LongAdder(); + longValue.add(number.longValue()); + return longValue; + } + } else if (Float.class == targetType) { + if (value instanceof Number) { + return ((Number) value).floatValue(); + } else if (value instanceof Boolean) { + return BooleanUtil.toFloatObj((Boolean) value); + } + final String valueStr = toStrFunc.apply((value)); + return StrUtil.isBlank(valueStr) ? null : NumberUtil.parseFloat(valueStr); + } else if (Double.class == targetType) { + if (value instanceof Number) { + return NumberUtil.toDouble((Number) value); + } else if (value instanceof Boolean) { + return BooleanUtil.toDoubleObj((Boolean) value); + } + final String valueStr = toStrFunc.apply((value)); + return StrUtil.isBlank(valueStr) ? null : NumberUtil.parseDouble(valueStr); + } else if (DoubleAdder.class == targetType) { + //jdk8 新增 + final Number number = convert(value, Double.class, toStrFunc); + if (null != number) { + final DoubleAdder doubleAdder = new DoubleAdder(); + doubleAdder.add(number.doubleValue()); + return doubleAdder; + } + } else if (BigDecimal.class == targetType) { + return toBigDecimal(value, toStrFunc); + } else if (BigInteger.class == targetType) { + return toBigInteger(value, toStrFunc); + } else if (Number.class == targetType) { + if (value instanceof Number) { + return (Number) value; + } else if (value instanceof Boolean) { + return BooleanUtil.toInteger((Boolean) value); + } + final String valueStr = toStrFunc.apply((value)); + return StrUtil.isBlank(valueStr) ? null : NumberUtil.parseNumber(valueStr); + } + + throw new UnsupportedOperationException(StrUtil.format("Unsupport Number type: {}", targetType.getName())); + } + + /** + * 转换为BigDecimal
+ * 如果给定的值为空,或者转换失败,返回默认值
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @param toStrFunc 转换为字符串的函数规则 + * @return 结果 + */ + private static BigDecimal toBigDecimal(Object value, Function toStrFunc) { + if (value instanceof Number) { + return NumberUtil.toBigDecimal((Number) value); + } else if (value instanceof Boolean) { + return ((boolean) value) ? BigDecimal.ONE : BigDecimal.ZERO; + } + + //对于Double类型,先要转换为String,避免精度问题 + return NumberUtil.toBigDecimal(toStrFunc.apply(value)); + } + + /** + * 转换为BigInteger
+ * 如果给定的值为空,或者转换失败,返回默认值
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @param toStrFunc 转换为字符串的函数规则 + * @return 结果 + */ + private static BigInteger toBigInteger(Object value, Function toStrFunc) { + if (value instanceof Long) { + return BigInteger.valueOf((Long) value); + } else if (value instanceof Boolean) { + return (boolean) value ? BigInteger.ONE : BigInteger.ZERO; + } + + return NumberUtil.toBigInteger(toStrFunc.apply(value)); + } +} diff --git a/src/main/java/cn/hutool/core/convert/impl/OptConverter.java b/src/main/java/cn/hutool/core/convert/impl/OptConverter.java new file mode 100644 index 0000000..cf79c17 --- /dev/null +++ b/src/main/java/cn/hutool/core/convert/impl/OptConverter.java @@ -0,0 +1,21 @@ +package cn.hutool.core.convert.impl; + +import cn.hutool.core.convert.AbstractConverter; +import cn.hutool.core.lang.Opt; + +/** + * + * {@link Opt}对象转换器 + * + * @author Looly + * @since 5.7.16 + */ +public class OptConverter extends AbstractConverter> { + private static final long serialVersionUID = 1L; + + @Override + protected Opt convertInternal(Object value) { + return Opt.ofNullable(value); + } + +} diff --git a/src/main/java/cn/hutool/core/convert/impl/OptionalConverter.java b/src/main/java/cn/hutool/core/convert/impl/OptionalConverter.java new file mode 100644 index 0000000..2c270ad --- /dev/null +++ b/src/main/java/cn/hutool/core/convert/impl/OptionalConverter.java @@ -0,0 +1,22 @@ +package cn.hutool.core.convert.impl; + +import cn.hutool.core.convert.AbstractConverter; + +import java.util.Optional; + +/** + * + * {@link Optional}对象转换器 + * + * @author Looly + * @since 5.0.0 + */ +public class OptionalConverter extends AbstractConverter> { + private static final long serialVersionUID = 1L; + + @Override + protected Optional convertInternal(Object value) { + return Optional.ofNullable(value); + } + +} diff --git a/src/main/java/cn/hutool/core/convert/impl/PathConverter.java b/src/main/java/cn/hutool/core/convert/impl/PathConverter.java new file mode 100644 index 0000000..a6ab97b --- /dev/null +++ b/src/main/java/cn/hutool/core/convert/impl/PathConverter.java @@ -0,0 +1,41 @@ +package cn.hutool.core.convert.impl; + +import java.io.File; +import java.net.URI; +import java.net.URL; +import java.nio.file.Path; +import java.nio.file.Paths; + +import cn.hutool.core.convert.AbstractConverter; + +/** + * 字符串转换器 + * @author Looly + * + */ +public class PathConverter extends AbstractConverter{ + private static final long serialVersionUID = 1L; + + @Override + protected Path convertInternal(Object value) { + try { + if(value instanceof URI){ + return Paths.get((URI)value); + } + + if(value instanceof URL){ + return Paths.get(((URL)value).toURI()); + } + + if(value instanceof File){ + return ((File)value).toPath(); + } + + return Paths.get(convertToStr(value)); + } catch (Exception e) { + // Ignore Exception + } + return null; + } + +} diff --git a/src/main/java/cn/hutool/core/convert/impl/PeriodConverter.java b/src/main/java/cn/hutool/core/convert/impl/PeriodConverter.java new file mode 100644 index 0000000..3c77dc2 --- /dev/null +++ b/src/main/java/cn/hutool/core/convert/impl/PeriodConverter.java @@ -0,0 +1,29 @@ +package cn.hutool.core.convert.impl; + +import cn.hutool.core.convert.AbstractConverter; + +import java.time.Period; +import java.time.temporal.TemporalAmount; + +/** + * + * {@link Period}对象转换器 + * + * @author Looly + * @since 5.0.0 + */ +public class PeriodConverter extends AbstractConverter { + private static final long serialVersionUID = 1L; + + @Override + protected Period convertInternal(Object value) { + if(value instanceof TemporalAmount){ + return Period.from((TemporalAmount) value); + }else if(value instanceof Integer){ + return Period.ofDays((Integer) value); + } else { + return Period.parse(convertToStr(value)); + } + } + +} diff --git a/src/main/java/cn/hutool/core/convert/impl/PrimitiveConverter.java b/src/main/java/cn/hutool/core/convert/impl/PrimitiveConverter.java new file mode 100644 index 0000000..bcc7689 --- /dev/null +++ b/src/main/java/cn/hutool/core/convert/impl/PrimitiveConverter.java @@ -0,0 +1,92 @@ +package cn.hutool.core.convert.impl; + +import cn.hutool.core.convert.AbstractConverter; +import cn.hutool.core.convert.Convert; +import cn.hutool.core.convert.ConvertException; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; + +import java.util.function.Function; + +/** + * 原始类型转换器
+ * 支持类型为:
+ *
    + *
  • {@code byte}
  • + *
  • {@code short}
  • + *
  • {@code int}
  • + *
  • {@code long}
  • + *
  • {@code float}
  • + *
  • {@code double}
  • + *
  • {@code char}
  • + *
  • {@code boolean}
  • + *
+ * + * @author Looly + */ +public class PrimitiveConverter extends AbstractConverter { + private static final long serialVersionUID = 1L; + + private final Class targetType; + + /** + * 构造
+ * + * @param clazz 需要转换的原始 + * @throws IllegalArgumentException 传入的转换类型非原始类型时抛出 + */ + public PrimitiveConverter(Class clazz) { + if (null == clazz) { + throw new NullPointerException("PrimitiveConverter not allow null target type!"); + } else if (!clazz.isPrimitive()) { + throw new IllegalArgumentException("[" + clazz + "] is not a primitive class!"); + } + this.targetType = clazz; + } + + @Override + protected Object convertInternal(Object value) { + return PrimitiveConverter.convert(value, this.targetType, this::convertToStr); + } + + @Override + protected String convertToStr(Object value) { + return StrUtil.trim(super.convertToStr(value)); + } + + @Override + @SuppressWarnings("unchecked") + public Class getTargetType() { + return (Class) this.targetType; + } + + /** + * 将指定值转换为原始类型的值 + * @param value 值 + * @param primitiveClass 原始类型 + * @param toStringFunc 当无法直接转换时,转为字符串后再转换的函数 + * @return 转换结果 + * @since 5.5.0 + */ + protected static Object convert(Object value, Class primitiveClass, Function toStringFunc) { + if (byte.class == primitiveClass) { + return ObjectUtil.defaultIfNull(NumberConverter.convert(value, Byte.class, toStringFunc), 0); + } else if (short.class == primitiveClass) { + return ObjectUtil.defaultIfNull(NumberConverter.convert(value, Short.class, toStringFunc), 0); + } else if (int.class == primitiveClass) { + return ObjectUtil.defaultIfNull(NumberConverter.convert(value, Integer.class, toStringFunc), 0); + } else if (long.class == primitiveClass) { + return ObjectUtil.defaultIfNull(NumberConverter.convert(value, Long.class, toStringFunc), 0); + } else if (float.class == primitiveClass) { + return ObjectUtil.defaultIfNull(NumberConverter.convert(value, Float.class, toStringFunc), 0); + } else if (double.class == primitiveClass) { + return ObjectUtil.defaultIfNull(NumberConverter.convert(value, Double.class, toStringFunc), 0); + } else if (char.class == primitiveClass) { + return Convert.convert(Character.class, value); + } else if (boolean.class == primitiveClass) { + return Convert.convert(Boolean.class, value); + } + + throw new ConvertException("Unsupported target type: {}", primitiveClass); + } +} diff --git a/src/main/java/cn/hutool/core/convert/impl/ReferenceConverter.java b/src/main/java/cn/hutool/core/convert/impl/ReferenceConverter.java new file mode 100644 index 0000000..7e13446 --- /dev/null +++ b/src/main/java/cn/hutool/core/convert/impl/ReferenceConverter.java @@ -0,0 +1,56 @@ +package cn.hutool.core.convert.impl; + +import cn.hutool.core.convert.AbstractConverter; +import cn.hutool.core.convert.ConverterRegistry; +import cn.hutool.core.util.StrUtil; +import cn.hutool.core.util.TypeUtil; + +import java.lang.ref.Reference; +import java.lang.ref.SoftReference; +import java.lang.ref.WeakReference; +import java.lang.reflect.Type; + +/** + * {@link Reference}转换器 + * + * @author Looly + * @since 3.0.8 + */ +@SuppressWarnings("rawtypes") +public class ReferenceConverter extends AbstractConverter { + private static final long serialVersionUID = 1L; + + private final Class targetType; + + /** + * 构造 + * @param targetType {@link Reference}实现类型 + */ + public ReferenceConverter(Class targetType) { + this.targetType = targetType; + } + + @SuppressWarnings("unchecked") + @Override + protected Reference convertInternal(Object value) { + + //尝试将值转换为Reference泛型的类型 + Object targetValue = null; + final Type paramType = TypeUtil.getTypeArgument(targetType); + if(!TypeUtil.isUnknown(paramType)){ + targetValue = ConverterRegistry.getInstance().convert(paramType, value); + } + if(null == targetValue){ + targetValue = value; + } + + if(this.targetType == WeakReference.class){ + return new WeakReference(targetValue); + }else if(this.targetType == SoftReference.class){ + return new SoftReference(targetValue); + } + + throw new UnsupportedOperationException(StrUtil.format("Unsupport Reference type: {}", this.targetType.getName())); + } + +} diff --git a/src/main/java/cn/hutool/core/convert/impl/StackTraceElementConverter.java b/src/main/java/cn/hutool/core/convert/impl/StackTraceElementConverter.java new file mode 100644 index 0000000..79d32f4 --- /dev/null +++ b/src/main/java/cn/hutool/core/convert/impl/StackTraceElementConverter.java @@ -0,0 +1,34 @@ +package cn.hutool.core.convert.impl; + +import java.util.Map; + +import cn.hutool.core.convert.AbstractConverter; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.ObjectUtil; + +/** + * {@link StackTraceElement} 转换器
+ * 只支持Map方式转换 + * + * @author Looly + * @since 3.0.8 + */ +public class StackTraceElementConverter extends AbstractConverter { + private static final long serialVersionUID = 1L; + + @Override + protected StackTraceElement convertInternal(Object value) { + if (value instanceof Map) { + final Map map = (Map) value; + + final String declaringClass = MapUtil.getStr(map, "className"); + final String methodName = MapUtil.getStr(map, "methodName"); + final String fileName = MapUtil.getStr(map, "fileName"); + final Integer lineNumber = MapUtil.getInt(map, "lineNumber"); + + return new StackTraceElement(declaringClass, methodName, fileName, ObjectUtil.defaultIfNull(lineNumber, 0)); + } + return null; + } + +} diff --git a/src/main/java/cn/hutool/core/convert/impl/StringConverter.java b/src/main/java/cn/hutool/core/convert/impl/StringConverter.java new file mode 100644 index 0000000..5aca871 --- /dev/null +++ b/src/main/java/cn/hutool/core/convert/impl/StringConverter.java @@ -0,0 +1,28 @@ +package cn.hutool.core.convert.impl; + +import cn.hutool.core.convert.AbstractConverter; + +import java.lang.reflect.Type; +import java.util.TimeZone; + +/** + * 字符串转换器,提供各种对象转换为字符串的逻辑封装 + * + * @author Looly + */ +public class StringConverter extends AbstractConverter { + private static final long serialVersionUID = 1L; + + @Override + protected String convertInternal(Object value) { + if (value instanceof TimeZone) { + return ((TimeZone) value).getID(); + } else if (value instanceof Type) { + return ((Type) value).getTypeName(); + } + + // 其它情况 + return convertToStr(value); + } + +} diff --git a/src/main/java/cn/hutool/core/convert/impl/TemporalAccessorConverter.java b/src/main/java/cn/hutool/core/convert/impl/TemporalAccessorConverter.java new file mode 100644 index 0000000..55a0d2f --- /dev/null +++ b/src/main/java/cn/hutool/core/convert/impl/TemporalAccessorConverter.java @@ -0,0 +1,308 @@ +package cn.hutool.core.convert.impl; + +import cn.hutool.core.convert.AbstractConverter; +import cn.hutool.core.convert.Convert; +import cn.hutool.core.convert.ConvertException; +import cn.hutool.core.date.DateTime; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.date.format.GlobalCustomFormat; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; + +import java.time.DayOfWeek; +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.Month; +import java.time.MonthDay; +import java.time.OffsetDateTime; +import java.time.OffsetTime; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.time.chrono.Era; +import java.time.chrono.IsoEra; +import java.time.format.DateTimeFormatter; +import java.time.temporal.TemporalAccessor; +import java.util.Calendar; +import java.util.Date; +import java.util.Map; +import java.util.Objects; + +/** + * JDK8中新加入的java.time包对象解析转换器
+ * 支持的对象包括: + * + *
+ * java.time.Instant
+ * java.time.LocalDateTime
+ * java.time.LocalDate
+ * java.time.LocalTime
+ * java.time.ZonedDateTime
+ * java.time.OffsetDateTime
+ * java.time.OffsetTime
+ * 
+ * + * @author looly + * @since 5.0.0 + */ +public class TemporalAccessorConverter extends AbstractConverter { + private static final long serialVersionUID = 1L; + + private final Class targetType; + /** + * 日期格式化 + */ + private String format; + + /** + * 构造 + * + * @param targetType 目标类型 + */ + public TemporalAccessorConverter(Class targetType) { + this(targetType, null); + } + + /** + * 构造 + * + * @param targetType 目标类型 + * @param format 日期格式 + */ + public TemporalAccessorConverter(Class targetType, String format) { + this.targetType = targetType; + this.format = format; + } + + /** + * 获取日期格式 + * + * @return 设置日期格式 + */ + public String getFormat() { + return format; + } + + /** + * 设置日期格式 + * + * @param format 日期格式 + */ + public void setFormat(String format) { + this.format = format; + } + + @SuppressWarnings("unchecked") + @Override + public Class getTargetType() { + return (Class) this.targetType; + } + + @Override + protected TemporalAccessor convertInternal(Object value) { + if (value instanceof Number) { + return parseFromLong(((Number) value).longValue()); + } else if (value instanceof TemporalAccessor) { + return parseFromTemporalAccessor((TemporalAccessor) value); + } else if (value instanceof Date) { + final DateTime dateTime = DateUtil.date((Date) value); + return parseFromInstant(dateTime.toInstant(), dateTime.getZoneId()); + } else if (value instanceof Calendar) { + final Calendar calendar = (Calendar) value; + return parseFromInstant(calendar.toInstant(), calendar.getTimeZone().toZoneId()); + } else if (value instanceof Map) { + final Map map = (Map) value; + if (LocalDate.class.equals(this.targetType)) { + return LocalDate.of(Convert.toInt(map.get("year")), Convert.toInt(map.get("month")), Convert.toInt(map.get("day"))); + } else if (LocalDateTime.class.equals(this.targetType)) { + return LocalDateTime.of(Convert.toInt(map.get("year")), Convert.toInt(map.get("month")), Convert.toInt(map.get("day")), + Convert.toInt(map.get("hour")), Convert.toInt(map.get("minute")), Convert.toInt(map.get("second")), Convert.toInt(map.get("second"))); + } else if (LocalTime.class.equals(this.targetType)) { + return LocalTime.of(Convert.toInt(map.get("hour")), Convert.toInt(map.get("minute")), Convert.toInt(map.get("second")), Convert.toInt(map.get("nano"))); + } + throw new ConvertException("Unsupported type: [{}] from map: [{}]", this.targetType, map); + } else { + return parseFromCharSequence(convertToStr(value)); + } + } + + /** + * 通过反射从字符串转java.time中的对象 + * + * @param value 字符串值 + * @return 日期对象 + */ + private TemporalAccessor parseFromCharSequence(CharSequence value) { + if (StrUtil.isBlank(value)) { + return null; + } + + if(DayOfWeek.class.equals(this.targetType)){ + return DayOfWeek.valueOf(StrUtil.toString(value)); + } else if(Month.class.equals(this.targetType)){ + return Month.valueOf(StrUtil.toString(value)); + } else if(Era.class.equals(this.targetType)){ + return IsoEra.valueOf(StrUtil.toString(value)); + } else if(MonthDay.class.equals(this.targetType)){ + return MonthDay.parse(value); + } + + final Instant instant; + ZoneId zoneId; + if (null != this.format) { + final DateTimeFormatter formatter = DateTimeFormatter.ofPattern(this.format); + instant = formatter.parse(value, Instant::from); + zoneId = formatter.getZone(); + } else { + final DateTime dateTime = DateUtil.parse(value); + instant = Objects.requireNonNull(dateTime).toInstant(); + zoneId = dateTime.getZoneId(); + } + return parseFromInstant(instant, zoneId); + } + + /** + * 将Long型时间戳转换为java.time中的对象 + * + * @param time 时间戳 + * @return java.time中的对象 + */ + private TemporalAccessor parseFromLong(Long time) { + if(DayOfWeek.class.equals(this.targetType)){ + return DayOfWeek.of(Math.toIntExact(time)); + } else if(Month.class.equals(this.targetType)){ + return Month.of(Math.toIntExact(time)); + } else if(Era.class.equals(this.targetType)){ + return IsoEra.of(Math.toIntExact(time)); + } + + final Instant instant; + if(GlobalCustomFormat.FORMAT_SECONDS.equals(this.format)){ + // https://gitee.com/dromara/hutool/issues/I6IS5B + // Unix时间戳 + instant = Instant.ofEpochSecond(time); + }else{ + instant = Instant.ofEpochMilli(time); + } + return parseFromInstant(instant, null); + } + + /** + * 将TemporalAccessor型时间戳转换为java.time中的对象 + * + * @param temporalAccessor TemporalAccessor对象 + * @return java.time中的对象 + */ + private TemporalAccessor parseFromTemporalAccessor(TemporalAccessor temporalAccessor) { + if(DayOfWeek.class.equals(this.targetType)){ + return DayOfWeek.from(temporalAccessor); + } else if(Month.class.equals(this.targetType)){ + return Month.from(temporalAccessor); + } else if(MonthDay.class.equals(this.targetType)){ + return MonthDay.from(temporalAccessor); + } + + TemporalAccessor result = null; + if (temporalAccessor instanceof LocalDateTime) { + result = parseFromLocalDateTime((LocalDateTime) temporalAccessor); + } else if (temporalAccessor instanceof ZonedDateTime) { + result = parseFromZonedDateTime((ZonedDateTime) temporalAccessor); + } + + if (null == result) { + result = parseFromInstant(DateUtil.toInstant(temporalAccessor), null); + } + + return result; + } + + /** + * 将TemporalAccessor型时间戳转换为java.time中的对象 + * + * @param localDateTime {@link LocalDateTime}对象 + * @return java.time中的对象 + */ + private TemporalAccessor parseFromLocalDateTime(LocalDateTime localDateTime) { + if (Instant.class.equals(this.targetType)) { + return DateUtil.toInstant(localDateTime); + } + if (LocalDate.class.equals(this.targetType)) { + return localDateTime.toLocalDate(); + } + if (LocalTime.class.equals(this.targetType)) { + return localDateTime.toLocalTime(); + } + if (ZonedDateTime.class.equals(this.targetType)) { + return localDateTime.atZone(ZoneId.systemDefault()); + } + if (OffsetDateTime.class.equals(this.targetType)) { + return localDateTime.atZone(ZoneId.systemDefault()).toOffsetDateTime(); + } + if (OffsetTime.class.equals(this.targetType)) { + return localDateTime.atZone(ZoneId.systemDefault()).toOffsetDateTime().toOffsetTime(); + } + + return null; + } + + /** + * 将TemporalAccessor型时间戳转换为java.time中的对象 + * + * @param zonedDateTime {@link ZonedDateTime}对象 + * @return java.time中的对象 + */ + private TemporalAccessor parseFromZonedDateTime(ZonedDateTime zonedDateTime) { + if (Instant.class.equals(this.targetType)) { + return DateUtil.toInstant(zonedDateTime); + } + if (LocalDateTime.class.equals(this.targetType)) { + return zonedDateTime.toLocalDateTime(); + } + if (LocalDate.class.equals(this.targetType)) { + return zonedDateTime.toLocalDate(); + } + if (LocalTime.class.equals(this.targetType)) { + return zonedDateTime.toLocalTime(); + } + if (OffsetDateTime.class.equals(this.targetType)) { + return zonedDateTime.toOffsetDateTime(); + } + if (OffsetTime.class.equals(this.targetType)) { + return zonedDateTime.toOffsetDateTime().toOffsetTime(); + } + + return null; + } + + /** + * 将TemporalAccessor型时间戳转换为java.time中的对象 + * + * @param instant {@link Instant}对象 + * @param zoneId 时区ID,null表示当前系统默认的时区 + * @return java.time中的对象 + */ + private TemporalAccessor parseFromInstant(Instant instant, ZoneId zoneId) { + if (Instant.class.equals(this.targetType)) { + return instant; + } + + zoneId = ObjectUtil.defaultIfNull(zoneId, ZoneId::systemDefault); + + TemporalAccessor result = null; + if (LocalDateTime.class.equals(this.targetType)) { + result = LocalDateTime.ofInstant(instant, zoneId); + } else if (LocalDate.class.equals(this.targetType)) { + result = instant.atZone(zoneId).toLocalDate(); + } else if (LocalTime.class.equals(this.targetType)) { + result = instant.atZone(zoneId).toLocalTime(); + } else if (ZonedDateTime.class.equals(this.targetType)) { + result = instant.atZone(zoneId); + } else if (OffsetDateTime.class.equals(this.targetType)) { + result = OffsetDateTime.ofInstant(instant, zoneId); + } else if (OffsetTime.class.equals(this.targetType)) { + result = OffsetTime.ofInstant(instant, zoneId); + } + return result; + } +} diff --git a/src/main/java/cn/hutool/core/convert/impl/TimeZoneConverter.java b/src/main/java/cn/hutool/core/convert/impl/TimeZoneConverter.java new file mode 100644 index 0000000..9072891 --- /dev/null +++ b/src/main/java/cn/hutool/core/convert/impl/TimeZoneConverter.java @@ -0,0 +1,20 @@ +package cn.hutool.core.convert.impl; + +import java.util.TimeZone; + +import cn.hutool.core.convert.AbstractConverter; + +/** + * TimeZone转换器 + * @author Looly + * + */ +public class TimeZoneConverter extends AbstractConverter{ + private static final long serialVersionUID = 1L; + + @Override + protected TimeZone convertInternal(Object value) { + return TimeZone.getTimeZone(convertToStr(value)); + } + +} diff --git a/src/main/java/cn/hutool/core/convert/impl/URIConverter.java b/src/main/java/cn/hutool/core/convert/impl/URIConverter.java new file mode 100644 index 0000000..94827aa --- /dev/null +++ b/src/main/java/cn/hutool/core/convert/impl/URIConverter.java @@ -0,0 +1,34 @@ +package cn.hutool.core.convert.impl; + +import java.io.File; +import java.net.URI; +import java.net.URL; + +import cn.hutool.core.convert.AbstractConverter; + +/** + * URI对象转换器 + * @author Looly + * + */ +public class URIConverter extends AbstractConverter{ + private static final long serialVersionUID = 1L; + + @Override + protected URI convertInternal(Object value) { + try { + if(value instanceof File){ + return ((File)value).toURI(); + } + + if(value instanceof URL){ + return ((URL)value).toURI(); + } + return new URI(convertToStr(value)); + } catch (Exception e) { + // Ignore Exception + } + return null; + } + +} diff --git a/src/main/java/cn/hutool/core/convert/impl/URLConverter.java b/src/main/java/cn/hutool/core/convert/impl/URLConverter.java new file mode 100644 index 0000000..df0a2b2 --- /dev/null +++ b/src/main/java/cn/hutool/core/convert/impl/URLConverter.java @@ -0,0 +1,34 @@ +package cn.hutool.core.convert.impl; + +import java.io.File; +import java.net.URI; +import java.net.URL; + +import cn.hutool.core.convert.AbstractConverter; + +/** + * URL对象转换器 + * @author Looly + * + */ +public class URLConverter extends AbstractConverter{ + private static final long serialVersionUID = 1L; + + @Override + protected URL convertInternal(Object value) { + try { + if(value instanceof File){ + return ((File)value).toURI().toURL(); + } + + if(value instanceof URI){ + return ((URI)value).toURL(); + } + return new URL(convertToStr(value)); + } catch (Exception e) { + // Ignore Exception + } + return null; + } + +} diff --git a/src/main/java/cn/hutool/core/convert/impl/UUIDConverter.java b/src/main/java/cn/hutool/core/convert/impl/UUIDConverter.java new file mode 100644 index 0000000..35a7d8b --- /dev/null +++ b/src/main/java/cn/hutool/core/convert/impl/UUIDConverter.java @@ -0,0 +1,22 @@ +package cn.hutool.core.convert.impl; + +import java.util.UUID; + +import cn.hutool.core.convert.AbstractConverter; + +/** + * UUID对象转换器转换器 + * + * @author Looly + * @since 4.0.10 + * + */ +public class UUIDConverter extends AbstractConverter { + private static final long serialVersionUID = 1L; + + @Override + protected UUID convertInternal(Object value) { + return UUID.fromString(convertToStr(value)); + } + +} diff --git a/src/main/java/cn/hutool/core/convert/impl/package-info.java b/src/main/java/cn/hutool/core/convert/impl/package-info.java new file mode 100644 index 0000000..d04c3c2 --- /dev/null +++ b/src/main/java/cn/hutool/core/convert/impl/package-info.java @@ -0,0 +1,7 @@ +/** + * 各种类型转换的实现类,其都为Converter接口的实现,用于将未知的Object类型转换为指定类型 + * + * @author looly + * + */ +package cn.hutool.core.convert.impl; \ No newline at end of file diff --git a/src/main/java/cn/hutool/core/convert/package-info.java b/src/main/java/cn/hutool/core/convert/package-info.java new file mode 100644 index 0000000..da721a9 --- /dev/null +++ b/src/main/java/cn/hutool/core/convert/package-info.java @@ -0,0 +1,7 @@ +/** + * 万能类型转换器以及各种类型转换的实现类,其中Convert为转换器入口,提供各种toXXX方法和convert方法 + * + * @author looly + * + */ +package cn.hutool.core.convert; \ No newline at end of file diff --git a/src/main/java/cn/hutool/core/date/BetweenFormatter.java b/src/main/java/cn/hutool/core/date/BetweenFormatter.java new file mode 100644 index 0000000..a24680a --- /dev/null +++ b/src/main/java/cn/hutool/core/date/BetweenFormatter.java @@ -0,0 +1,208 @@ +package cn.hutool.core.date; + +import cn.hutool.core.util.StrUtil; + +import java.io.Serializable; + +/** + * 时长格式化器,用于格式化输出两个日期相差的时长
+ * 根据{@link Level}不同,调用{@link #format()}方法后返回类似于: + *
    + *
  • XX小时XX分XX秒
  • + *
  • XX天XX小时
  • + *
  • XX月XX天XX小时
  • + *
+ * + * @author Looly + */ +public class BetweenFormatter implements Serializable { + private static final long serialVersionUID = 1L; + + /** + * 时长毫秒数 + */ + private long betweenMs; + /** + * 格式化级别 + */ + private Level level; + /** + * 格式化级别的最大个数 + */ + private final int levelMaxCount; + + /** + * 构造 + * + * @param betweenMs 日期间隔 + * @param level 级别,按照天、小时、分、秒、毫秒分为5个等级,根据传入等级,格式化到相应级别 + */ + public BetweenFormatter(long betweenMs, Level level) { + this(betweenMs, level, 0); + } + + /** + * 构造 + * + * @param betweenMs 日期间隔 + * @param level 级别,按照天、小时、分、秒、毫秒分为5个等级,根据传入等级,格式化到相应级别 + * @param levelMaxCount 格式化级别的最大个数,假如级别个数为1,但是级别到秒,那只显示一个级别 + */ + public BetweenFormatter(long betweenMs, Level level, int levelMaxCount) { + this.betweenMs = betweenMs; + this.level = level; + this.levelMaxCount = levelMaxCount; + } + + /** + * 格式化日期间隔输出
+ * + * @return 格式化后的字符串 + */ + public String format() { + final StringBuilder sb = new StringBuilder(); + if (betweenMs > 0) { + long day = betweenMs / DateUnit.DAY.getMillis(); + long hour = betweenMs / DateUnit.HOUR.getMillis() - day * 24; + long minute = betweenMs / DateUnit.MINUTE.getMillis() - day * 24 * 60 - hour * 60; + + final long BetweenOfSecond = ((day * 24 + hour) * 60 + minute) * 60; + long second = betweenMs / DateUnit.SECOND.getMillis() - BetweenOfSecond; + long millisecond = betweenMs - (BetweenOfSecond + second) * 1000; + + final int level = this.level.ordinal(); + int levelCount = 0; + + if (isLevelCountValid(levelCount) && 0 != day && level >= Level.DAY.ordinal()) { + sb.append(day).append(Level.DAY.name); + levelCount++; + } + if (isLevelCountValid(levelCount) && 0 != hour && level >= Level.HOUR.ordinal()) { + sb.append(hour).append(Level.HOUR.name); + levelCount++; + } + if (isLevelCountValid(levelCount) && 0 != minute && level >= Level.MINUTE.ordinal()) { + sb.append(minute).append(Level.MINUTE.name); + levelCount++; + } + if (isLevelCountValid(levelCount) && 0 != second && level >= Level.SECOND.ordinal()) { + sb.append(second).append(Level.SECOND.name); + levelCount++; + } + if (isLevelCountValid(levelCount) && 0 != millisecond && level >= Level.MILLISECOND.ordinal()) { + sb.append(millisecond).append(Level.MILLISECOND.name); + // levelCount++; + } + } + + if (StrUtil.isEmpty(sb)) { + sb.append(0).append(this.level.name); + } + + return sb.toString(); + } + + /** + * 获得 时长毫秒数 + * + * @return 时长毫秒数 + */ + public long getBetweenMs() { + return betweenMs; + } + + /** + * 设置 时长毫秒数 + * + * @param betweenMs 时长毫秒数 + */ + public void setBetweenMs(long betweenMs) { + this.betweenMs = betweenMs; + } + + /** + * 获得 格式化级别 + * + * @return 格式化级别 + */ + public Level getLevel() { + return level; + } + + /** + * 设置格式化级别 + * + * @param level 格式化级别 + */ + public void setLevel(Level level) { + this.level = level; + } + + /** + * 格式化等级枚举 + * + * @author Looly + */ + public enum Level { + + /** + * 天 + */ + DAY("天"), + /** + * 小时 + */ + HOUR("小时"), + /** + * 分钟 + */ + MINUTE("分"), + /** + * 秒 + */ + SECOND("秒"), + /** + * 毫秒 + */ + MILLISECOND("毫秒"); + + /** + * 级别名称 + */ + private final String name; + + /** + * 构造 + * + * @param name 级别名称 + */ + Level(String name) { + this.name = name; + } + + /** + * 获取级别名称 + * + * @return 级别名称 + */ + public String getName() { + return this.name; + } + } + + @Override + public String toString() { + return format(); + } + + /** + * 等级数量是否有效
+ * 有效的定义是:levelMaxCount大于0(被设置),当前等级数量没有超过这个最大值 + * + * @param levelCount 登记数量 + * @return 是否有效 + */ + private boolean isLevelCountValid(int levelCount) { + return this.levelMaxCount <= 0 || levelCount < this.levelMaxCount; + } +} diff --git a/src/main/java/cn/hutool/core/date/CalendarUtil.java b/src/main/java/cn/hutool/core/date/CalendarUtil.java new file mode 100644 index 0000000..f5cfed4 --- /dev/null +++ b/src/main/java/cn/hutool/core/date/CalendarUtil.java @@ -0,0 +1,772 @@ +package cn.hutool.core.date; + +import cn.hutool.core.comparator.CompareUtil; +import cn.hutool.core.convert.NumberChineseFormatter; +import cn.hutool.core.date.format.DateParser; +import cn.hutool.core.date.format.FastDateParser; +import cn.hutool.core.date.format.GlobalCustomFormat; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; + +import java.text.ParsePosition; +import java.time.Instant; +import java.time.LocalDateTime; +import java.util.Calendar; +import java.util.Date; +import java.util.LinkedHashSet; +import java.util.Locale; +import java.util.TimeZone; + +/** + * 针对{@link Calendar} 对象封装工具类 + * + * @author looly + * @since 5.3.0 + */ +public class CalendarUtil { + + /** + * 创建Calendar对象,时间为默认时区的当前时间 + * + * @return Calendar对象 + * @since 4.6.6 + */ + public static Calendar calendar() { + return Calendar.getInstance(); + } + + /** + * 转换为Calendar对象 + * + * @param date 日期对象 + * @return Calendar对象 + */ + public static Calendar calendar(Date date) { + if (date instanceof DateTime) { + return ((DateTime) date).toCalendar(); + } else { + return calendar(date.getTime()); + } + } + + /** + * 转换为Calendar对象,使用当前默认时区 + * + * @param millis 时间戳 + * @return Calendar对象 + */ + public static Calendar calendar(long millis) { + return calendar(millis, TimeZone.getDefault()); + } + + /** + * 转换为Calendar对象 + * + * @param millis 时间戳 + * @param timeZone 时区 + * @return Calendar对象 + * @since 5.7.22 + */ + public static Calendar calendar(long millis, TimeZone timeZone) { + final Calendar cal = Calendar.getInstance(timeZone); + cal.setTimeInMillis(millis); + return cal; + } + + /** + * 是否为上午 + * + * @param calendar {@link Calendar} + * @return 是否为上午 + */ + public static boolean isAM(Calendar calendar) { + return Calendar.AM == calendar.get(Calendar.AM_PM); + } + + /** + * 是否为下午 + * + * @param calendar {@link Calendar} + * @return 是否为下午 + */ + public static boolean isPM(Calendar calendar) { + return Calendar.PM == calendar.get(Calendar.AM_PM); + } + + /** + * 修改日期为某个时间字段起始时间 + * + * @param calendar {@link Calendar} + * @param dateField 保留到的时间字段,如定义为 {@link DateField#SECOND},表示这个字段不变,这个字段以下字段全部归0 + * @return 原{@link Calendar} + */ + public static Calendar truncate(Calendar calendar, DateField dateField) { + return DateModifier.modify(calendar, dateField.getValue(), DateModifier.ModifyType.TRUNCATE); + } + + /** + * 修改日期为某个时间字段四舍五入时间 + * + * @param calendar {@link Calendar} + * @param dateField 时间字段 + * @return 原{@link Calendar} + */ + public static Calendar round(Calendar calendar, DateField dateField) { + return DateModifier.modify(calendar, dateField.getValue(), DateModifier.ModifyType.ROUND); + } + + /** + * 修改日期为某个时间字段结束时间 + * + * @param calendar {@link Calendar} + * @param dateField 保留到的时间字段,如定义为 {@link DateField#SECOND},表示这个字段不变,这个字段以下字段全部取最大值 + * @return 原{@link Calendar} + */ + public static Calendar ceiling(Calendar calendar, DateField dateField) { + return DateModifier.modify(calendar, dateField.getValue(), DateModifier.ModifyType.CEILING); + } + + /** + * 修改日期为某个时间字段结束时间
+ * 可选是否归零毫秒。 + * + *

+ * 有时候由于毫秒部分必须为0(如MySQL数据库中),因此在此加上选项。 + *

+ * + * @param calendar {@link Calendar} + * @param dateField 时间字段 + * @param truncateMillisecond 是否毫秒归零 + * @return 原{@link Calendar} + */ + public static Calendar ceiling(Calendar calendar, DateField dateField, boolean truncateMillisecond) { + return DateModifier.modify(calendar, dateField.getValue(), DateModifier.ModifyType.CEILING, truncateMillisecond); + } + + /** + * 修改秒级别的开始时间,即忽略毫秒部分 + * + * @param calendar 日期 {@link Calendar} + * @return {@link Calendar} + * @since 4.6.2 + */ + public static Calendar beginOfSecond(Calendar calendar) { + return truncate(calendar, DateField.SECOND); + } + + /** + * 修改秒级别的结束时间,即毫秒设置为999 + * + * @param calendar 日期 {@link Calendar} + * @return {@link Calendar} + * @since 4.6.2 + */ + public static Calendar endOfSecond(Calendar calendar) { + return ceiling(calendar, DateField.SECOND); + } + + /** + * 修改某小时的开始时间 + * + * @param calendar 日期 {@link Calendar} + * @return {@link Calendar} + */ + public static Calendar beginOfHour(Calendar calendar) { + return truncate(calendar, DateField.HOUR_OF_DAY); + } + + /** + * 修改某小时的结束时间 + * + * @param calendar 日期 {@link Calendar} + * @return {@link Calendar} + */ + public static Calendar endOfHour(Calendar calendar) { + return ceiling(calendar, DateField.HOUR_OF_DAY); + } + + /** + * 修改某分钟的开始时间 + * + * @param calendar 日期 {@link Calendar} + * @return {@link Calendar} + */ + public static Calendar beginOfMinute(Calendar calendar) { + return truncate(calendar, DateField.MINUTE); + } + + /** + * 修改某分钟的结束时间 + * + * @param calendar 日期 {@link Calendar} + * @return {@link Calendar} + */ + public static Calendar endOfMinute(Calendar calendar) { + return ceiling(calendar, DateField.MINUTE); + } + + /** + * 修改某天的开始时间 + * + * @param calendar 日期 {@link Calendar} + * @return {@link Calendar} + */ + public static Calendar beginOfDay(Calendar calendar) { + return truncate(calendar, DateField.DAY_OF_MONTH); + } + + /** + * 修改某天的结束时间 + * + * @param calendar 日期 {@link Calendar} + * @return {@link Calendar} + */ + public static Calendar endOfDay(Calendar calendar) { + return ceiling(calendar, DateField.DAY_OF_MONTH); + } + + /** + * 修改给定日期当前周的开始时间,周一定为一周的开始时间 + * + * @param calendar 日期 {@link Calendar} + * @return {@link Calendar} + */ + public static Calendar beginOfWeek(Calendar calendar) { + return beginOfWeek(calendar, true); + } + + /** + * 修改给定日期当前周的开始时间 + * + * @param calendar 日期 {@link Calendar} + * @param isMondayAsFirstDay 是否周一做为一周的第一天(false表示周日做为第一天) + * @return {@link Calendar} + * @since 3.1.2 + */ + public static Calendar beginOfWeek(Calendar calendar, boolean isMondayAsFirstDay) { + calendar.setFirstDayOfWeek(isMondayAsFirstDay ? Calendar.MONDAY : Calendar.SUNDAY); + // WEEK_OF_MONTH为上限的字段(不包括),实际调整的为DAY_OF_MONTH + return truncate(calendar, DateField.WEEK_OF_MONTH); + } + + /** + * 修改某周的结束时间,周日定为一周的结束 + * + * @param calendar 日期 {@link Calendar} + * @return {@link Calendar} + */ + public static Calendar endOfWeek(Calendar calendar) { + return endOfWeek(calendar, true); + } + + /** + * 修改某周的结束时间 + * + * @param calendar 日期 {@link Calendar} + * @param isSundayAsLastDay 是否周日做为一周的最后一天(false表示周六做为最后一天) + * @return {@link Calendar} + */ + public static Calendar endOfWeek(Calendar calendar, boolean isSundayAsLastDay) { + calendar.setFirstDayOfWeek(isSundayAsLastDay ? Calendar.MONDAY : Calendar.SUNDAY); + // WEEK_OF_MONTH为上限的字段(不包括),实际调整的为DAY_OF_MONTH + return ceiling(calendar, DateField.WEEK_OF_MONTH); + } + + /** + * 修改某月的开始时间 + * + * @param calendar 日期 {@link Calendar} + * @return {@link Calendar} + */ + public static Calendar beginOfMonth(Calendar calendar) { + return truncate(calendar, DateField.MONTH); + } + + /** + * 修改某月的结束时间 + * + * @param calendar 日期 {@link Calendar} + * @return {@link Calendar} + */ + public static Calendar endOfMonth(Calendar calendar) { + return ceiling(calendar, DateField.MONTH); + } + + /** + * 修改某季度的开始时间 + * + * @param calendar 日期 {@link Calendar} + * @return {@link Calendar} + * @since 4.1.0 + */ + public static Calendar beginOfQuarter(Calendar calendar) { + //noinspection MagicConstant + calendar.set(Calendar.MONTH, calendar.get(DateField.MONTH.getValue()) / 3 * 3); + calendar.set(Calendar.DAY_OF_MONTH, 1); + return beginOfDay(calendar); + } + + /** + * 获取某季度的结束时间 + * + * @param calendar 日期 {@link Calendar} + * @return {@link Calendar} + * @since 4.1.0 + */ + @SuppressWarnings({"MagicConstant", "ConstantConditions"}) + public static Calendar endOfQuarter(Calendar calendar) { + final int year = calendar.get(Calendar.YEAR); + final int month = calendar.get(DateField.MONTH.getValue()) / 3 * 3 + 2; + + final Calendar resultCal = Calendar.getInstance(calendar.getTimeZone()); + resultCal.set(year, month, Month.of(month).getLastDay(DateUtil.isLeapYear(year))); + + return endOfDay(resultCal); + } + + /** + * 修改某年的开始时间 + * + * @param calendar 日期 {@link Calendar} + * @return {@link Calendar} + */ + public static Calendar beginOfYear(Calendar calendar) { + return truncate(calendar, DateField.YEAR); + } + + /** + * 修改某年的结束时间 + * + * @param calendar 日期 {@link Calendar} + * @return {@link Calendar} + */ + public static Calendar endOfYear(Calendar calendar) { + return ceiling(calendar, DateField.YEAR); + } + + /** + * 比较两个日期是否为同一天 + * + * @param cal1 日期1 + * @param cal2 日期2 + * @return 是否为同一天 + */ + public static boolean isSameDay(Calendar cal1, Calendar cal2) { + if (cal1 == null || cal2 == null) { + throw new IllegalArgumentException("The date must not be null"); + } + return cal1.get(Calendar.DAY_OF_YEAR) == cal2.get(Calendar.DAY_OF_YEAR) && // + cal1.get(Calendar.YEAR) == cal2.get(Calendar.YEAR) && // + cal1.get(Calendar.ERA) == cal2.get(Calendar.ERA); + } + + /** + * 比较两个日期是否为同一周 + * + * @param cal1 日期1 + * @param cal2 日期2 + * @param isMon 是否为周一。国内第一天为星期一,国外第一天为星期日 + * @return 是否为同一周 + * @since 5.7.21 + */ + public static boolean isSameWeek(Calendar cal1, Calendar cal2, boolean isMon) { + if (cal1 == null || cal2 == null) { + throw new IllegalArgumentException("The date must not be null"); + } + + // 防止比较前修改原始Calendar对象 + cal1 = (Calendar) cal1.clone(); + cal2 = (Calendar) cal2.clone(); + + // 把所传日期设置为其当前周的第一天 + // 比较设置后的两个日期是否是同一天:true 代表同一周 + if (isMon) { + cal1.setFirstDayOfWeek(Calendar.MONDAY); + cal1.set(Calendar.DAY_OF_WEEK, Calendar.MONDAY); + cal2.setFirstDayOfWeek(Calendar.MONDAY); + cal2.set(Calendar.DAY_OF_WEEK, Calendar.MONDAY); + } else { + cal1.setFirstDayOfWeek(Calendar.SUNDAY); + cal1.set(Calendar.DAY_OF_WEEK, Calendar.SUNDAY); + cal2.setFirstDayOfWeek(Calendar.SUNDAY); + cal2.set(Calendar.DAY_OF_WEEK, Calendar.SUNDAY); + } + return isSameDay(cal1, cal2); + } + + /** + * 比较两个日期是否为同一月 + * + * @param cal1 日期1 + * @param cal2 日期2 + * @return 是否为同一月 + * @since 5.4.1 + */ + public static boolean isSameMonth(Calendar cal1, Calendar cal2) { + if (cal1 == null || cal2 == null) { + throw new IllegalArgumentException("The date must not be null"); + } + return cal1.get(Calendar.YEAR) == cal2.get(Calendar.YEAR) && // + cal1.get(Calendar.MONTH) == cal2.get(Calendar.MONTH); + } + + /** + *

检查两个Calendar时间戳是否相同。

+ * + *

此方法检查两个Calendar的毫秒数时间戳是否相同。

+ * + * @param date1 时间1 + * @param date2 时间2 + * @return 两个Calendar时间戳是否相同。如果两个时间都为{@code null}返回true,否则有{@code null}返回false + * @since 5.3.11 + */ + public static boolean isSameInstant(Calendar date1, Calendar date2) { + if (null == date1) { + return null == date2; + } + if (null == date2) { + return false; + } + + return date1.getTimeInMillis() == date2.getTimeInMillis(); + } + + /** + * 获得指定日期区间内的年份和季度
+ * + * @param startDate 起始日期(包含) + * @param endDate 结束日期(包含) + * @return 季度列表 ,元素类似于 20132 + * @since 4.1.15 + */ + public static LinkedHashSet yearAndQuarter(long startDate, long endDate) { + LinkedHashSet quarters = new LinkedHashSet<>(); + final Calendar cal = calendar(startDate); + while (startDate <= endDate) { + // 如果开始时间超出结束时间,让结束时间为开始时间,处理完后结束循环 + quarters.add(yearAndQuarter(cal)); + + cal.add(Calendar.MONTH, 3); + startDate = cal.getTimeInMillis(); + } + + return quarters; + } + + /** + * 获得指定日期年份和季度
+ * 格式:[20131]表示2013年第一季度 + * + * @param cal 日期 + * @return 年和季度,格式类似于20131 + */ + public static String yearAndQuarter(Calendar cal) { + return StrUtil.builder().append(cal.get(Calendar.YEAR)).append(cal.get(Calendar.MONTH) / 3 + 1).toString(); + } + + /** + * 获取指定日期字段的最小值,例如分钟的最小值是0 + * + * @param calendar {@link Calendar} + * @param dateField {@link DateField} + * @return 字段最小值 + * @see Calendar#getActualMinimum(int) + * @since 5.4.2 + */ + public static int getBeginValue(Calendar calendar, DateField dateField) { + return getBeginValue(calendar, dateField.getValue()); + } + + /** + * 获取指定日期字段的最小值,例如分钟的最小值是0 + * + * @param calendar {@link Calendar} + * @param dateField {@link DateField} + * @return 字段最小值 + * @see Calendar#getActualMinimum(int) + * @since 4.5.7 + */ + public static int getBeginValue(Calendar calendar, int dateField) { + if (Calendar.DAY_OF_WEEK == dateField) { + return calendar.getFirstDayOfWeek(); + } + return calendar.getActualMinimum(dateField); + } + + /** + * 获取指定日期字段的最大值,例如分钟的最大值是59 + * + * @param calendar {@link Calendar} + * @param dateField {@link DateField} + * @return 字段最大值 + * @see Calendar#getActualMaximum(int) + * @since 5.4.2 + */ + public static int getEndValue(Calendar calendar, DateField dateField) { + return getEndValue(calendar, dateField.getValue()); + } + + /** + * 获取指定日期字段的最大值,例如分钟的最大值是59 + * + * @param calendar {@link Calendar} + * @param dateField {@link DateField} + * @return 字段最大值 + * @see Calendar#getActualMaximum(int) + * @since 4.5.7 + */ + public static int getEndValue(Calendar calendar, int dateField) { + if (Calendar.DAY_OF_WEEK == dateField) { + return (calendar.getFirstDayOfWeek() + 6) % 7; + } + return calendar.getActualMaximum(dateField); + } + + /** + * Calendar{@link Instant}对象 + * + * @param calendar Date对象 + * @return {@link Instant}对象 + * @since 5.0.5 + */ + public static Instant toInstant(Calendar calendar) { + return null == calendar ? null : calendar.toInstant(); + } + + /** + * {@link Calendar} 转换为 {@link LocalDateTime},使用系统默认时区 + * + * @param calendar {@link Calendar} + * @return {@link LocalDateTime} + * @since 5.0.5 + */ + public static LocalDateTime toLocalDateTime(Calendar calendar) { + return LocalDateTime.ofInstant(calendar.toInstant(), calendar.getTimeZone().toZoneId()); + } + + /** + * {@code null}安全的{@link Calendar}比较,{@code null}小于任何日期 + * + * @param calendar1 日期1 + * @param calendar2 日期2 + * @return 比较结果,如果calendar1 < calendar2,返回数小于0,calendar1==calendar2返回0,calendar1 > calendar2 大于0 + * @since 4.6.2 + */ + public static int compare(Calendar calendar1, Calendar calendar2) { + return CompareUtil.compare(calendar1, calendar2); + } + + /** + * 计算相对于dateToCompare的年龄,长用于计算指定生日在某年的年龄 + * + * @param birthday 生日 + * @param dateToCompare 需要对比的日期 + * @return 年龄 + */ + public static int age(Calendar birthday, Calendar dateToCompare) { + return age(birthday.getTimeInMillis(), dateToCompare.getTimeInMillis()); + } + + /** + * 将指定Calendar时间格式化为纯中文形式,比如: + * + *
+	 *     2018-02-24 12:13:14 转换为 二〇一八年二月二十四日(withTime为false)
+	 *     2018-02-24 12:13:14 转换为 二〇一八年二月二十四日十二时十三分十四秒(withTime为true)
+	 * 
+ * + * @param calendar {@link Calendar} + * @param withTime 是否包含时间部分 + * @return 格式化后的字符串 + * @since 5.3.9 + */ + public static String formatChineseDate(Calendar calendar, boolean withTime) { + final StringBuilder result = StrUtil.builder(); + + // 年 + final String year = String.valueOf(calendar.get(Calendar.YEAR)); + final int length = year.length(); + for (int i = 0; i < length; i++) { + result.append(NumberChineseFormatter.numberCharToChinese(year.charAt(i), false)); + } + result.append('年'); + + // 月 + int month = calendar.get(Calendar.MONTH) + 1; + result.append(NumberChineseFormatter.formatThousand(month, false)); + result.append('月'); + + // 日 + int day = calendar.get(Calendar.DAY_OF_MONTH); + result.append(NumberChineseFormatter.formatThousand(day, false)); + result.append('日'); + + // 只替换年月日,时分秒中零不需要替换 + String temp = result.toString().replace('零', '〇'); + result.delete(0, result.length()); + result.append(temp); + + + if (withTime) { + // 时 + int hour = calendar.get(Calendar.HOUR_OF_DAY); + result.append(NumberChineseFormatter.formatThousand(hour, false)); + result.append('时'); + // 分 + int minute = calendar.get(Calendar.MINUTE); + result.append(NumberChineseFormatter.formatThousand(minute, false)); + result.append('分'); + // 秒 + int second = calendar.get(Calendar.SECOND); + result.append(NumberChineseFormatter.formatThousand(second, false)); + result.append('秒'); + } + + return result.toString(); + } + + /** + * 计算相对于dateToCompare的年龄,常用于计算指定生日在某年的年龄 + * + * @param birthday 生日 + * @param dateToCompare 需要对比的日期 + * @return 年龄 + */ + protected static int age(long birthday, long dateToCompare) { + if (birthday > dateToCompare) { + throw new IllegalArgumentException("Birthday is after dateToCompare!"); + } + + final Calendar cal = Calendar.getInstance(); + cal.setTimeInMillis(dateToCompare); + + final int year = cal.get(Calendar.YEAR); + final int month = cal.get(Calendar.MONTH); + final int dayOfMonth = cal.get(Calendar.DAY_OF_MONTH); + final boolean isLastDayOfMonth = dayOfMonth == cal.getActualMaximum(Calendar.DAY_OF_MONTH); + + cal.setTimeInMillis(birthday); + int age = year - cal.get(Calendar.YEAR); + + final int monthBirth = cal.get(Calendar.MONTH); + + //当前日期,则为0岁 + if (age == 0){ + return 0; + } else if (month == monthBirth) { + + final int dayOfMonthBirth = cal.get(Calendar.DAY_OF_MONTH); + final boolean isLastDayOfMonthBirth = dayOfMonthBirth == cal.getActualMaximum(Calendar.DAY_OF_MONTH); + if ((!isLastDayOfMonth || !isLastDayOfMonthBirth) && dayOfMonth <= dayOfMonthBirth) { + // 如果生日在当月,但是未超过生日当天的日期,年龄减一 + age--; + } + } else if (month < monthBirth) { + // 如果当前月份未达到生日的月份,年龄计算减一 + age--; + } + + return age; + } + + /** + * 通过给定的日期格式解析日期时间字符串。
+ * 传入的日期格式会逐个尝试,直到解析成功,返回{@link Calendar}对象,否则抛出{@link DateException}异常。 + * 方法来自:Apache Commons-Lang3 + * + * @param str 日期时间字符串,非空 + * @param parsePatterns 需要尝试的日期时间格式数组,非空, 见SimpleDateFormat + * @return 解析后的Calendar + * @throws IllegalArgumentException if the date string or pattern array is null + * @throws DateException if none of the date patterns were suitable + * @since 5.3.11 + */ + public static Calendar parseByPatterns(String str, String... parsePatterns) throws DateException { + return parseByPatterns(str, null, parsePatterns); + } + + /** + * 通过给定的日期格式解析日期时间字符串。
+ * 传入的日期格式会逐个尝试,直到解析成功,返回{@link Calendar}对象,否则抛出{@link DateException}异常。 + * 方法来自:Apache Commons-Lang3 + * + * @param str 日期时间字符串,非空 + * @param locale 地区,当为{@code null}时使用{@link Locale#getDefault()} + * @param parsePatterns 需要尝试的日期时间格式数组,非空, 见SimpleDateFormat + * @return 解析后的Calendar + * @throws IllegalArgumentException if the date string or pattern array is null + * @throws DateException if none of the date patterns were suitable + * @since 5.3.11 + */ + public static Calendar parseByPatterns(String str, Locale locale, String... parsePatterns) throws DateException { + return parseByPatterns(str, locale, true, parsePatterns); + } + + /** + * 通过给定的日期格式解析日期时间字符串。
+ * 传入的日期格式会逐个尝试,直到解析成功,返回{@link Calendar}对象,否则抛出{@link DateException}异常。 + * 方法来自:Apache Commons-Lang3 + * + * @param str 日期时间字符串,非空 + * @param locale 地区,当为{@code null}时使用{@link Locale#getDefault()} + * @param lenient 日期时间解析是否使用严格模式 + * @param parsePatterns 需要尝试的日期时间格式数组,非空, 见SimpleDateFormat + * @return 解析后的Calendar + * @throws IllegalArgumentException if the date string or pattern array is null + * @throws DateException if none of the date patterns were suitable + * @see Calendar#isLenient() + * @since 5.3.11 + */ + public static Calendar parseByPatterns(String str, Locale locale, boolean lenient, String... parsePatterns) throws DateException { + if (str == null || parsePatterns == null) { + throw new IllegalArgumentException("Date and Patterns must not be null"); + } + + final TimeZone tz = TimeZone.getDefault(); + final Locale lcl = ObjectUtil.defaultIfNull(locale, Locale.getDefault()); + final ParsePosition pos = new ParsePosition(0); + final Calendar calendar = Calendar.getInstance(tz, lcl); + calendar.setLenient(lenient); + + for (final String parsePattern : parsePatterns) { + if (GlobalCustomFormat.isCustomFormat(parsePattern)) { + final Date parse = GlobalCustomFormat.parse(str, parsePattern); + if (null == parse) { + continue; + } + calendar.setTime(parse); + return calendar; + } + + final FastDateParser fdp = new FastDateParser(parsePattern, tz, lcl); + calendar.clear(); + try { + if (fdp.parse(str, pos, calendar) && pos.getIndex() == str.length()) { + return calendar; + } + } catch (final IllegalArgumentException ignore) { + // leniency is preventing calendar from being set + } + pos.setIndex(0); + } + + throw new DateException("Unable to parse the date: {}", str); + } + + /** + * 使用指定{@link DateParser}解析字符串为{@link Calendar} + * + * @param str 日期字符串 + * @param lenient 是否宽容模式 + * @param parser {@link DateParser} + * @return 解析后的 {@link Calendar},解析失败返回{@code null} + * @since 5.7.14 + */ + public static Calendar parse(CharSequence str, boolean lenient, DateParser parser) { + final Calendar calendar = Calendar.getInstance(parser.getTimeZone(), parser.getLocale()); + calendar.clear(); + calendar.setLenient(lenient); + + return parser.parse(StrUtil.str(str), new ParsePosition(0), calendar) ? calendar : null; + } +} diff --git a/src/main/java/cn/hutool/core/date/ChineseDate.java b/src/main/java/cn/hutool/core/date/ChineseDate.java new file mode 100644 index 0000000..aa2abcf --- /dev/null +++ b/src/main/java/cn/hutool/core/date/ChineseDate.java @@ -0,0 +1,462 @@ +package cn.hutool.core.date; + +import cn.hutool.core.convert.NumberChineseFormatter; +import cn.hutool.core.date.chinese.ChineseMonth; +import cn.hutool.core.date.chinese.GanZhi; +import cn.hutool.core.date.chinese.LunarFestival; +import cn.hutool.core.date.chinese.LunarInfo; +import cn.hutool.core.date.chinese.SolarTerms; +import cn.hutool.core.util.StrUtil; + +import java.time.LocalDate; +import java.util.Calendar; +import java.util.Date; + + +/** + * 农历日期工具,最大支持到2099年,支持: + * + *
    + *
  • 通过公历日期构造获取对应农历
  • + *
  • 通过农历日期直接构造
  • + *
+ * + * @author zjw, looly + * @since 5.1.1 + */ +public class ChineseDate { + + //农历年 + private final int year; + //农历月,润N月这个值就是N+1,其他月按照显示月份赋值 + private final int month; + // 当前月份是否闰月 + private final boolean isLeapMonth; + //农历日 + private final int day; + + //公历年 + private final int gyear; + //公历月,从1开始计数 + private final int gmonthBase1; + //公历日 + private final int gday; + + /** + * 通过公历日期构造 + * + * @param date 公历日期 + */ + public ChineseDate(Date date) { + this(LocalDateTimeUtil.ofDate(date.toInstant())); + } + + /** + * 通过公历日期构造 + * + * @param localDate 公历日期 + * @since 5.7.22 + */ + public ChineseDate(LocalDate localDate) { + // 公历 + gyear = localDate.getYear(); + gmonthBase1 = localDate.getMonthValue(); + gday = localDate.getDayOfMonth(); + + // 求出和1900年1月31日相差的天数 + int offset = (int) (localDate.toEpochDay() - LunarInfo.BASE_DAY); + + // 计算农历年份 + // 用offset减去每农历年的天数,计算当天是农历第几天,offset是当年的第几天 + int daysOfYear; + int iYear; + for (iYear = LunarInfo.BASE_YEAR; iYear <= LunarInfo.MAX_YEAR; iYear++) { + daysOfYear = LunarInfo.yearDays(iYear); + if (offset < daysOfYear) { + break; + } + offset -= daysOfYear; + } + + year = iYear; + // 计算农历月份 + final int leapMonth = LunarInfo.leapMonth(iYear); // 闰哪个月,1-12 + // 用当年的天数offset,逐个减去每月(农历)的天数,求出当天是本月的第几天 + int month; + int daysOfMonth; + boolean hasLeapMonth = false; + for (month = 1; month < 13; month++) { + // 闰月,如润的是五月,则5表示五月,6表示润五月 + if (leapMonth > 0 && month == (leapMonth + 1)) { + daysOfMonth = LunarInfo.leapDays(year); + hasLeapMonth = true; + } else { + // 普通月,当前面的月份存在闰月时,普通月份要-1,递补闰月的数字 + // 如2月是闰月,此时3月实际是第四个月 + daysOfMonth = LunarInfo.monthDays(year, hasLeapMonth ? month - 1 : month); + } + + if (offset < daysOfMonth) { + // offset不足月,结束 + break; + } + offset -= daysOfMonth; + } + + this.isLeapMonth = leapMonth > 0 && (month == (leapMonth + 1)); + if (hasLeapMonth && !this.isLeapMonth) { + // 当前月份前有闰月,则月份显示要-1,除非当前月份就是润月 + month--; + } + this.month = month; + this.day = offset + 1; + } + + /** + * 构造方法传入日期
+ * 此方法自动判断闰月,如果chineseMonth为本年的闰月,则按照闰月计算 + * + * @param chineseYear 农历年 + * @param chineseMonth 农历月,1表示一月(正月) + * @param chineseDay 农历日,1表示初一 + * @since 5.2.4 + */ + public ChineseDate(int chineseYear, int chineseMonth, int chineseDay) { + this(chineseYear, chineseMonth, chineseDay, chineseMonth == LunarInfo.leapMonth(chineseYear)); + } + + /** + * 构造方法传入日期
+ * 通过isLeapMonth参数区分是否闰月,如五月是闰月,当isLeapMonth为{@code true}时,表示润五月,{@code false}表示五月 + * + * @param chineseYear 农历年 + * @param chineseMonth 农历月,1表示一月(正月),如果isLeapMonth为{@code true},1表示润一月 + * @param chineseDay 农历日,1表示初一 + * @param isLeapMonth 当前月份是否闰月 + * @since 5.7.18 + */ + public ChineseDate(int chineseYear, int chineseMonth, int chineseDay, boolean isLeapMonth) { + if(chineseMonth != LunarInfo.leapMonth(chineseYear)){ + // issue#I5YB1A,用户传入的月份可能非闰月,此时此参数无效。 + isLeapMonth = false; + } + + this.day = chineseDay; + // 当月是闰月的后边的月定义为闰月,如润的是五月,则5表示五月,6表示润五月 + this.isLeapMonth = isLeapMonth; + // 闰月时,农历月份+1,如6表示润五月 + this.month = isLeapMonth ? chineseMonth + 1 : chineseMonth; + this.year = chineseYear; + + final DateTime dateTime = lunar2solar(chineseYear, chineseMonth, chineseDay, isLeapMonth); + if (null != dateTime) { + //初始化公历年 + this.gday = dateTime.dayOfMonth(); + //初始化公历月 + this.gmonthBase1 = dateTime.month() + 1; + //初始化公历日 + this.gyear = dateTime.year(); + } else { + //初始化公历年 + this.gday = -1; + //初始化公历月 + this.gmonthBase1 = -1; + //初始化公历日 + this.gyear = -1; + } + } + + /** + * 获得农历年份 + * + * @return 返回农历年份 + */ + public int getChineseYear() { + return this.year; + } + + /** + * 获取公历的年 + * + * @return 公历年 + * @since 5.6.1 + */ + public int getGregorianYear() { + return this.gyear; + } + + /** + * 获取农历的月,从1开始计数
+ * 此方法返回实际的月序号,如一月是闰月,则一月返回1,润一月返回2 + * + * @return 农历的月 + * @since 5.2.4 + */ + public int getMonth() { + return this.month; + } + + /** + * 获取公历的月,从1开始计数 + * + * @return 公历月 + * @since 5.6.1 + */ + public int getGregorianMonthBase1() { + return this.gmonthBase1; + } + + /** + * 获取公历的月,从0开始计数 + * + * @return 公历月 + * @since 5.6.1 + */ + public int getGregorianMonth() { + return this.gmonthBase1 - 1; + } + + /** + * 当前农历月份是否为闰月 + * + * @return 是否为闰月 + * @since 5.4.2 + */ + public boolean isLeapMonth() { + return this.isLeapMonth; + } + + + /** + * 获得农历月份(中文,例如二月,十二月,或者润一月) + * + * @return 返回农历月份 + */ + public String getChineseMonth() { + return getChineseMonth(false); + } + + /** + * 获得农历月称呼(中文,例如二月,腊月,或者润正月) + * + * @return 返回农历月份称呼 + */ + public String getChineseMonthName() { + return getChineseMonth(true); + } + + /** + * 获得农历月份(中文,例如二月,十二月,或者润一月) + * + * @param isTraditional 是否传统表示,例如一月传统表示为正月 + * @return 返回农历月份 + * @since 5.7.18 + */ + public String getChineseMonth(boolean isTraditional) { + return ChineseMonth.getChineseMonthName(isLeapMonth(), + isLeapMonth() ? this.month - 1 : this.month, isTraditional); + } + + /** + * 获取农历的日,从1开始计数 + * + * @return 农历的日,从1开始计数 + * @since 5.2.4 + */ + public int getDay() { + return this.day; + } + + /** + * 获取公历的日 + * + * @return 公历日 + * @since 5.6.1 + */ + public int getGregorianDay() { + return this.gday; + } + + /** + * 获得农历日 + * + * @return 获得农历日 + */ + public String getChineseDay() { + String[] chineseTen = {"初", "十", "廿", "卅"}; + int n = (day % 10 == 0) ? 9 : (day % 10 - 1); + if (day > 30) { + return ""; + } + switch (day) { + case 10: + return "初十"; + case 20: + return "二十"; + case 30: + return "三十"; + default: + return chineseTen[day / 10] + NumberChineseFormatter.format(n + 1, false); + } + } + + /** + * 获取公历的Date + * + * @return 公历Date + * @since 5.6.1 + */ + public Date getGregorianDate() { + return DateUtil.date(getGregorianCalendar()); + } + + /** + * 获取公历的Calendar + * + * @return 公历Calendar + * @since 5.6.1 + */ + public Calendar getGregorianCalendar() { + final Calendar calendar = CalendarUtil.calendar(); + //noinspection MagicConstant + calendar.set(this.gyear, getGregorianMonth(), this.gday, 0, 0, 0); + return calendar; + } + + /** + * 获得节日,闰月不计入节日中 + * + * @return 获得农历节日 + */ + public String getFestivals() { + return StrUtil.join(",", LunarFestival.getFestivals(this.year, this.month, day)); + } + + /** + * 获得年份生肖 + * + * @return 获得年份生肖 + */ + public String getChineseZodiac() { + return Zodiac.getChineseZodiac(this.year); + } + + + /** + * 获得年的天干地支 + * + * @return 获得天干地支 + */ + public String getCyclical() { + return GanZhi.getGanzhiOfYear(this.year); + } + + /** + * 干支纪年信息 + * + * @return 获得天干地支的年月日信息 + */ + public String getCyclicalYMD() { + if (gyear >= LunarInfo.BASE_YEAR && gmonthBase1 > 0 && gday > 0) { + return cyclicalm(gyear, gmonthBase1, gday); + } + return null; + } + + + /** + * 获得节气 + * + * @return 获得节气 + * @since 5.6.3 + */ + public String getTerm() { + return SolarTerms.getTerm(gyear, gmonthBase1, gday); + } + + /** + * 转换为标准的日期格式来表示农历日期,例如2020-01-13
+ * 如果存在闰月,显示闰月月份,如润二月显示2 + * + * @return 标准的日期格式 + * @since 5.2.4 + */ + public String toStringNormal() { + return String.format("%04d-%02d-%02d", this.year, + isLeapMonth() ? this.month - 1 : this.month, this.day); + } + + @Override + public String toString() { + return String.format("%s%s年 %s%s", getCyclical(), getChineseZodiac(), getChineseMonthName(), getChineseDay()); + } + + // ------------------------------------------------------- private method start + + /** + * 这里同步处理年月日的天干地支信息 + * + * @param year 公历年 + * @param month 公历月,从1开始 + * @param day 公历日 + * @return 天干地支信息 + */ + private String cyclicalm(int year, int month, int day) { + return StrUtil.format("{}年{}月{}日", + GanZhi.getGanzhiOfYear(this.year), + GanZhi.getGanzhiOfMonth(year, month, day), + GanZhi.getGanzhiOfDay(year, month, day)); + } + + /** + * 通过农历年月日信息 返回公历信息 提供给构造函数 + * + * @param chineseYear 农历年 + * @param chineseMonth 农历月 + * @param chineseDay 农历日 + * @param isLeapMonth 传入的月是不是闰月 + * @return 公历信息 + */ + private DateTime lunar2solar(int chineseYear, int chineseMonth, int chineseDay, boolean isLeapMonth) { + //超出了最大极限值 + if ((chineseYear == 2100 && chineseMonth == 12 && chineseDay > 1) || + (chineseYear == LunarInfo.BASE_YEAR && chineseMonth == 1 && chineseDay < 31)) { + return null; + } + int day = LunarInfo.monthDays(chineseYear, chineseMonth); + int _day = day; + if (isLeapMonth) { + _day = LunarInfo.leapDays(chineseYear); + } + //参数合法性效验 + if (chineseYear < LunarInfo.BASE_YEAR || chineseYear > 2100 || chineseDay > _day) { + return null; + } + //计算农历的时间差 + int offset = 0; + for (int i = LunarInfo.BASE_YEAR; i < chineseYear; i++) { + offset += LunarInfo.yearDays(i); + } + int leap; + boolean isAdd = false; + for (int i = 1; i < chineseMonth; i++) { + leap = LunarInfo.leapMonth(chineseYear); + if (!isAdd) {//处理闰月 + if (leap <= i && leap > 0) { + offset += LunarInfo.leapDays(chineseYear); + isAdd = true; + } + } + offset += LunarInfo.monthDays(chineseYear, i); + } + //转换闰月农历 需补充该年闰月的前一个月的时差 + if (isLeapMonth) { + offset += day; + } + //1900年农历正月一日的公历时间为1900年1月30日0时0分0秒(该时间也是本农历的最开始起始点) -2203804800000 + return DateUtil.date(((offset + chineseDay - 31) * 86400000L) - 2203804800000L); + } + + // ------------------------------------------------------- private method end + +} diff --git a/src/main/java/cn/hutool/core/date/DateBetween.java b/src/main/java/cn/hutool/core/date/DateBetween.java new file mode 100644 index 0000000..691aaae --- /dev/null +++ b/src/main/java/cn/hutool/core/date/DateBetween.java @@ -0,0 +1,185 @@ +package cn.hutool.core.date; + +import cn.hutool.core.lang.Assert; + +import java.io.Serializable; +import java.util.Calendar; +import java.util.Date; + +/** + * 日期间隔 + * + * @author Looly + */ +public class DateBetween implements Serializable { + private static final long serialVersionUID = 1L; + + /** + * 开始日期 + */ + private final Date begin; + /** + * 结束日期 + */ + private final Date end; + + /** + * 创建
+ * 在前的日期做为起始时间,在后的做为结束时间,间隔只保留绝对值正数 + * + * @param begin 起始时间 + * @param end 结束时间 + * @return DateBetween + * @since 3.2.3 + */ + public static DateBetween create(Date begin, Date end) { + return new DateBetween(begin, end); + } + + /** + * 创建
+ * 在前的日期做为起始时间,在后的做为结束时间,间隔只保留绝对值正数 + * + * @param begin 起始时间 + * @param end 结束时间 + * @param isAbs 日期间隔是否只保留绝对值正数 + * @return DateBetween + * @since 3.2.3 + */ + public static DateBetween create(Date begin, Date end, boolean isAbs) { + return new DateBetween(begin, end, isAbs); + } + + /** + * 构造
+ * 在前的日期做为起始时间,在后的做为结束时间,间隔只保留绝对值正数 + * + * @param begin 起始时间 + * @param end 结束时间 + */ + public DateBetween(Date begin, Date end) { + this(begin, end, true); + } + + /** + * 构造
+ * 在前的日期做为起始时间,在后的做为结束时间 + * + * @param begin 起始时间 + * @param end 结束时间 + * @param isAbs 日期间隔是否只保留绝对值正数 + * @since 3.1.1 + */ + public DateBetween(Date begin, Date end, boolean isAbs) { + Assert.notNull(begin, "Begin date is null !"); + Assert.notNull(end, "End date is null !"); + + if (isAbs && begin.after(end)) { + // 间隔只为正数的情况下,如果开始日期晚于结束日期,置换之 + this.begin = end; + this.end = begin; + } else { + this.begin = begin; + this.end = end; + } + } + + /** + * 判断两个日期相差的时长
+ * 返回 给定单位的时长差 + * + * @param unit 相差的单位:相差 天{@link DateUnit#DAY}、小时{@link DateUnit#HOUR} 等 + * @return 时长差 + */ + public long between(DateUnit unit) { + long diff = end.getTime() - begin.getTime(); + return diff / unit.getMillis(); + } + + /** + * 计算两个日期相差月数
+ * 在非重置情况下,如果起始日期的天大于结束日期的天,月数要少算1(不足1个月) + * + * @param isReset 是否重置时间为起始时间(重置天时分秒) + * @return 相差月数 + * @since 3.0.8 + */ + public long betweenMonth(boolean isReset) { + final Calendar beginCal = DateUtil.calendar(begin); + final Calendar endCal = DateUtil.calendar(end); + + final int betweenYear = endCal.get(Calendar.YEAR) - beginCal.get(Calendar.YEAR); + final int betweenMonthOfYear = endCal.get(Calendar.MONTH) - beginCal.get(Calendar.MONTH); + + int result = betweenYear * 12 + betweenMonthOfYear; + if (!isReset) { + endCal.set(Calendar.YEAR, beginCal.get(Calendar.YEAR)); + endCal.set(Calendar.MONTH, beginCal.get(Calendar.MONTH)); + long between = endCal.getTimeInMillis() - beginCal.getTimeInMillis(); + if (between < 0) { + return result - 1; + } + } + return result; + } + + /** + * 计算两个日期相差年数
+ * 在非重置情况下,如果起始日期的月大于结束日期的月,年数要少算1(不足1年) + * + * @param isReset 是否重置时间为起始时间(重置月天时分秒) + * @return 相差年数 + * @since 3.0.8 + */ + public long betweenYear(boolean isReset) { + final Calendar beginCal = DateUtil.calendar(begin); + final Calendar endCal = DateUtil.calendar(end); + + int result = endCal.get(Calendar.YEAR) - beginCal.get(Calendar.YEAR); + if (!isReset) { + // 考虑闰年的2月情况 + if (Calendar.FEBRUARY == beginCal.get(Calendar.MONTH) && Calendar.FEBRUARY == endCal.get(Calendar.MONTH)) { + if (beginCal.get(Calendar.DAY_OF_MONTH) == beginCal.getActualMaximum(Calendar.DAY_OF_MONTH) + && endCal.get(Calendar.DAY_OF_MONTH) == endCal.getActualMaximum(Calendar.DAY_OF_MONTH)) { + // 两个日期都位于2月的最后一天,此时月数按照相等对待,此时都设置为1号 + beginCal.set(Calendar.DAY_OF_MONTH, 1); + endCal.set(Calendar.DAY_OF_MONTH, 1); + } + } + + endCal.set(Calendar.YEAR, beginCal.get(Calendar.YEAR)); + long between = endCal.getTimeInMillis() - beginCal.getTimeInMillis(); + if (between < 0) { + return result - 1; + } + } + return result; + } + + /** + * 格式化输出时间差 + * + * @param unit 日期单位 + * @param level 级别 + * @return 字符串 + * @since 5.7.17 + */ + public String toString(DateUnit unit, BetweenFormatter.Level level) { + return DateUtil.formatBetween(between(unit), level); + } + + /** + * 格式化输出时间差 + * + * @param level 级别 + * @return 字符串 + */ + public String toString(BetweenFormatter.Level level) { + return toString(DateUnit.MS, level); + } + + @Override + public String toString() { + return toString(BetweenFormatter.Level.MILLISECOND); + } +} diff --git a/src/main/java/cn/hutool/core/date/DateException.java b/src/main/java/cn/hutool/core/date/DateException.java new file mode 100644 index 0000000..b22ec6f --- /dev/null +++ b/src/main/java/cn/hutool/core/date/DateException.java @@ -0,0 +1,32 @@ +package cn.hutool.core.date; + +import cn.hutool.core.exceptions.ExceptionUtil; +import cn.hutool.core.util.StrUtil; + +/** + * 工具类异常 + * @author xiaoleilu + */ +public class DateException extends RuntimeException{ + private static final long serialVersionUID = 8247610319171014183L; + + public DateException(Throwable e) { + super(ExceptionUtil.getMessage(e), e); + } + + public DateException(String message) { + super(message); + } + + public DateException(String messageTemplate, Object... params) { + super(StrUtil.format(messageTemplate, params)); + } + + public DateException(String message, Throwable throwable) { + super(message, throwable); + } + + public DateException(Throwable throwable, String messageTemplate, Object... params) { + super(StrUtil.format(messageTemplate, params), throwable); + } +} diff --git a/src/main/java/cn/hutool/core/date/DateField.java b/src/main/java/cn/hutool/core/date/DateField.java new file mode 100644 index 0000000..cf1a749 --- /dev/null +++ b/src/main/java/cn/hutool/core/date/DateField.java @@ -0,0 +1,158 @@ +package cn.hutool.core.date; + +import java.util.Calendar; + +/** + * 日期各个部分的枚举
+ * 与Calendar相应值对应 + * + * @author Looly + * + */ +public enum DateField { + + /** + * 世纪 + * + * @see Calendar#ERA + */ + ERA(Calendar.ERA), + /** + * 年 + * + * @see Calendar#YEAR + */ + YEAR(Calendar.YEAR), + /** + * 月 + * + * @see Calendar#MONTH + */ + MONTH(Calendar.MONTH), + /** + * 一年中第几周 + * + * @see Calendar#WEEK_OF_YEAR + */ + WEEK_OF_YEAR(Calendar.WEEK_OF_YEAR), + /** + * 一月中第几周 + * + * @see Calendar#WEEK_OF_MONTH + */ + WEEK_OF_MONTH(Calendar.WEEK_OF_MONTH), + /** + * 一月中的第几天 + * + * @see Calendar#DAY_OF_MONTH + */ + DAY_OF_MONTH(Calendar.DAY_OF_MONTH), + /** + * 一年中的第几天 + * + * @see Calendar#DAY_OF_YEAR + */ + DAY_OF_YEAR(Calendar.DAY_OF_YEAR), + /** + * 周几,1表示周日,2表示周一 + * + * @see Calendar#DAY_OF_WEEK + */ + DAY_OF_WEEK(Calendar.DAY_OF_WEEK), + /** + * 天所在的周是这个月的第几周 + * + * @see Calendar#DAY_OF_WEEK_IN_MONTH + */ + DAY_OF_WEEK_IN_MONTH(Calendar.DAY_OF_WEEK_IN_MONTH), + /** + * 上午或者下午 + * + * @see Calendar#AM_PM + */ + AM_PM(Calendar.AM_PM), + /** + * 小时,用于12小时制 + * + * @see Calendar#HOUR + */ + HOUR(Calendar.HOUR), + /** + * 小时,用于24小时制 + * + * @see Calendar#HOUR + */ + HOUR_OF_DAY(Calendar.HOUR_OF_DAY), + /** + * 分钟 + * + * @see Calendar#MINUTE + */ + MINUTE(Calendar.MINUTE), + /** + * 秒 + * + * @see Calendar#SECOND + */ + SECOND(Calendar.SECOND), + /** + * 毫秒 + * + * @see Calendar#MILLISECOND + */ + MILLISECOND(Calendar.MILLISECOND); + + // --------------------------------------------------------------- + private final int value; + + DateField(int value) { + this.value = value; + } + + public int getValue() { + return this.value; + } + + /** + * 将 {@link Calendar}相关值转换为DatePart枚举对象
+ * + * @param calendarPartIntValue Calendar中关于Week的int值 + * @return DateField + */ + public static DateField of(int calendarPartIntValue) { + switch (calendarPartIntValue) { + case Calendar.ERA: + return ERA; + case Calendar.YEAR: + return YEAR; + case Calendar.MONTH: + return MONTH; + case Calendar.WEEK_OF_YEAR: + return WEEK_OF_YEAR; + case Calendar.WEEK_OF_MONTH: + return WEEK_OF_MONTH; + case Calendar.DAY_OF_MONTH: + return DAY_OF_MONTH; + case Calendar.DAY_OF_YEAR: + return DAY_OF_YEAR; + case Calendar.DAY_OF_WEEK: + return DAY_OF_WEEK; + case Calendar.DAY_OF_WEEK_IN_MONTH: + return DAY_OF_WEEK_IN_MONTH; + case Calendar.AM_PM: + return AM_PM; + case Calendar.HOUR: + return HOUR; + case Calendar.HOUR_OF_DAY: + return HOUR_OF_DAY; + case Calendar.MINUTE: + return MINUTE; + case Calendar.SECOND: + return SECOND; + case Calendar.MILLISECOND: + return MILLISECOND; + default: + return null; + } + } +} diff --git a/src/main/java/cn/hutool/core/date/DateModifier.java b/src/main/java/cn/hutool/core/date/DateModifier.java new file mode 100644 index 0000000..0fa9663 --- /dev/null +++ b/src/main/java/cn/hutool/core/date/DateModifier.java @@ -0,0 +1,176 @@ +package cn.hutool.core.date; + +import cn.hutool.core.util.ArrayUtil; + +import java.util.Calendar; + +/** + * 日期修改器
+ * 用于实现自定义某个日期字段的调整,包括: + * + *
+ * 1. 获取指定字段的起始时间
+ * 2. 获取指定字段的四舍五入时间
+ * 3. 获取指定字段的结束时间
+ * 
+ * + * @author looly + */ +public class DateModifier { + + /** + * 忽略的计算的字段 + */ + private static final int[] IGNORE_FIELDS = new int[]{ // + Calendar.HOUR_OF_DAY, // 与HOUR同名 + Calendar.AM_PM, // 此字段单独处理,不参与计算起始和结束 + Calendar.DAY_OF_WEEK_IN_MONTH, // 不参与计算 + Calendar.DAY_OF_YEAR, // DAY_OF_MONTH体现 + Calendar.WEEK_OF_MONTH, // 特殊处理 + Calendar.WEEK_OF_YEAR // WEEK_OF_MONTH体现 + }; + + /** + * 修改日期 + * + * @param calendar {@link Calendar} + * @param dateField 日期字段,即保留到哪个日期字段 + * @param modifyType 修改类型,包括舍去、四舍五入、进一等 + * @return 修改后的{@link Calendar} + */ + public static Calendar modify(Calendar calendar, int dateField, ModifyType modifyType) { + return modify(calendar, dateField, modifyType, false); + } + + /** + * 修改日期,取起始值或者结束值
+ * 可选是否归零毫秒。 + * + *

+ * 在{@link ModifyType#TRUNCATE}模式下,毫秒始终要归零, + * 但是在{@link ModifyType#CEILING}和{@link ModifyType#ROUND}模式下, + * 有时候由于毫秒部分必须为0(如MySQL数据库中),因此在此加上选项。 + *

+ * + * @param calendar {@link Calendar} + * @param dateField 日期字段,即保留到哪个日期字段 + * @param modifyType 修改类型,包括舍去、四舍五入、进一等 + * @param truncateMillisecond 是否归零毫秒 + * @return 修改后的{@link Calendar} + * @since 5.7.5 + */ + public static Calendar modify(Calendar calendar, int dateField, ModifyType modifyType, boolean truncateMillisecond) { + // AM_PM上下午特殊处理 + if (Calendar.AM_PM == dateField) { + boolean isAM = DateUtil.isAM(calendar); + switch (modifyType) { + case TRUNCATE: + calendar.set(Calendar.HOUR_OF_DAY, isAM ? 0 : 12); + break; + case CEILING: + calendar.set(Calendar.HOUR_OF_DAY, isAM ? 11 : 23); + break; + case ROUND: + int min = isAM ? 0 : 12; + int max = isAM ? 11 : 23; + int href = (max - min) / 2 + 1; + int value = calendar.get(Calendar.HOUR_OF_DAY); + calendar.set(Calendar.HOUR_OF_DAY, (value < href) ? min : max); + break; + } + // 处理下一级别字段 + return modify(calendar, dateField + 1, modifyType); + } + + final int endField = truncateMillisecond ? Calendar.SECOND : Calendar.MILLISECOND; + // 循环处理各级字段,精确到毫秒字段 + for (int i = dateField + 1; i <= endField; i++) { + if (ArrayUtil.contains(IGNORE_FIELDS, i)) { + // 忽略无关字段(WEEK_OF_MONTH)始终不做修改 + continue; + } + + // 在计算本周的起始和结束日时,月相关的字段忽略。 + if (Calendar.WEEK_OF_MONTH == dateField || Calendar.WEEK_OF_YEAR == dateField) { + if (Calendar.DAY_OF_MONTH == i) { + continue; + } + } else { + // 其它情况忽略周相关字段计算 + if (Calendar.DAY_OF_WEEK == i) { + continue; + } + } + + modifyField(calendar, i, modifyType); + } + + if (truncateMillisecond) { + calendar.set(Calendar.MILLISECOND, 0); + } + + return calendar; + } + + // -------------------------------------------------------------------------------------------------- Private method start + + /** + * 修改日期字段值 + * + * @param calendar {@link Calendar} + * @param field 字段,见{@link Calendar} + * @param modifyType {@link ModifyType} + */ + private static void modifyField(Calendar calendar, int field, ModifyType modifyType) { + if (Calendar.HOUR == field) { + // 修正小时。HOUR为12小时制,上午的结束时间为12:00,此处改为HOUR_OF_DAY: 23:59 + field = Calendar.HOUR_OF_DAY; + } + + switch (modifyType) { + case TRUNCATE: + calendar.set(field, DateUtil.getBeginValue(calendar, field)); + break; + case CEILING: + calendar.set(field, DateUtil.getEndValue(calendar, field)); + break; + case ROUND: + int min = DateUtil.getBeginValue(calendar, field); + int max = DateUtil.getEndValue(calendar, field); + int href; + if (Calendar.DAY_OF_WEEK == field) { + // 星期特殊处理,假设周一是第一天,中间的为周四 + href = (min + 3) % 7; + } else { + href = (max - min) / 2 + 1; + } + int value = calendar.get(field); + calendar.set(field, (value < href) ? min : max); + break; + } + // Console.log("# {} -> {}", DateField.of(field), calendar.get(field)); + } + // -------------------------------------------------------------------------------------------------- Private method end + + /** + * 修改类型 + * + * @author looly + */ + public enum ModifyType { + /** + * 取指定日期短的起始值. + */ + TRUNCATE, + + /** + * 指定日期属性按照四舍五入处理 + */ + ROUND, + + /** + * 指定日期属性按照进一法处理 + */ + CEILING + } +} diff --git a/src/main/java/cn/hutool/core/date/DatePattern.java b/src/main/java/cn/hutool/core/date/DatePattern.java new file mode 100644 index 0000000..3a94d65 --- /dev/null +++ b/src/main/java/cn/hutool/core/date/DatePattern.java @@ -0,0 +1,322 @@ +package cn.hutool.core.date; + +import cn.hutool.core.date.format.FastDateFormat; + +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.util.Locale; +import java.util.TimeZone; +import java.util.regex.Pattern; + +/** + * 日期格式化类,提供常用的日期格式化对象 + * + * @author Looly + */ +public class DatePattern { + + /** + * 标准日期时间正则,每个字段支持单个数字或2个数字,包括: + *
+	 *     yyyy-MM-dd HH:mm:ss.SSSSSS
+	 *     yyyy-MM-dd HH:mm:ss.SSS
+	 *     yyyy-MM-dd HH:mm:ss
+	 *     yyyy-MM-dd HH:mm
+	 *     yyyy-MM-dd
+	 * 
+ * + * @since 5.3.6 + */ + public static final Pattern REGEX_NORM = Pattern.compile("\\d{4}-\\d{1,2}-\\d{1,2}(\\s\\d{1,2}:\\d{1,2}(:\\d{1,2})?)?(.\\d{1,6})?"); + + //-------------------------------------------------------------------------------------------------------------------------------- Normal + /** + * 年格式:yyyy + */ + public static final String NORM_YEAR_PATTERN = "yyyy"; + /** + * 年月格式:yyyy-MM + */ + public static final String NORM_MONTH_PATTERN = "yyyy-MM"; + /** + * 年月格式 {@link FastDateFormat}:yyyy-MM + */ + public static final FastDateFormat NORM_MONTH_FORMAT = FastDateFormat.getInstance(NORM_MONTH_PATTERN); + /** + * 年月格式 {@link FastDateFormat}:yyyy-MM + */ + public static final DateTimeFormatter NORM_MONTH_FORMATTER = createFormatter(NORM_MONTH_PATTERN); + + /** + * 简单年月格式:yyyyMM + */ + public static final String SIMPLE_MONTH_PATTERN = "yyyyMM"; + /** + * 简单年月格式 {@link FastDateFormat}:yyyyMM + */ + public static final FastDateFormat SIMPLE_MONTH_FORMAT = FastDateFormat.getInstance(SIMPLE_MONTH_PATTERN); + /** + * 简单年月格式 {@link FastDateFormat}:yyyyMM + */ + public static final DateTimeFormatter SIMPLE_MONTH_FORMATTER = createFormatter(SIMPLE_MONTH_PATTERN); + + /** + * 标准日期格式:yyyy-MM-dd + */ + public static final String NORM_DATE_PATTERN = "yyyy-MM-dd"; + /** + * 标准日期格式 {@link FastDateFormat}:yyyy-MM-dd + */ + public static final FastDateFormat NORM_DATE_FORMAT = FastDateFormat.getInstance(NORM_DATE_PATTERN); + /** + * 标准日期格式 {@link FastDateFormat}:yyyy-MM-dd + */ + public static final DateTimeFormatter NORM_DATE_FORMATTER = createFormatter(NORM_DATE_PATTERN); + + /** + * 标准时间格式:HH:mm:ss + */ + public static final String NORM_TIME_PATTERN = "HH:mm:ss"; + /** + * 标准时间格式 {@link FastDateFormat}:HH:mm:ss + */ + public static final FastDateFormat NORM_TIME_FORMAT = FastDateFormat.getInstance(NORM_TIME_PATTERN); + /** + * 标准日期格式 {@link FastDateFormat}:HH:mm:ss + */ + public static final DateTimeFormatter NORM_TIME_FORMATTER = createFormatter(NORM_TIME_PATTERN); + + /** + * 标准日期时间格式,精确到分:yyyy-MM-dd HH:mm + */ + public static final String NORM_DATETIME_MINUTE_PATTERN = "yyyy-MM-dd HH:mm"; + /** + * 标准日期时间格式,精确到分 {@link FastDateFormat}:yyyy-MM-dd HH:mm + */ + public static final FastDateFormat NORM_DATETIME_MINUTE_FORMAT = FastDateFormat.getInstance(NORM_DATETIME_MINUTE_PATTERN); + /** + * 标准日期格式 {@link FastDateFormat}:yyyy-MM-dd HH:mm + */ + public static final DateTimeFormatter NORM_DATETIME_MINUTE_FORMATTER = createFormatter(NORM_DATETIME_MINUTE_PATTERN); + + /** + * 标准日期时间格式,精确到秒:yyyy-MM-dd HH:mm:ss + */ + public static final String NORM_DATETIME_PATTERN = "yyyy-MM-dd HH:mm:ss"; + /** + * 标准日期时间格式,精确到秒 {@link FastDateFormat}:yyyy-MM-dd HH:mm:ss + */ + public static final FastDateFormat NORM_DATETIME_FORMAT = FastDateFormat.getInstance(NORM_DATETIME_PATTERN); + /** + * 标准日期时间格式,精确到秒 {@link FastDateFormat}:yyyy-MM-dd HH:mm:ss + */ + public static final DateTimeFormatter NORM_DATETIME_FORMATTER = createFormatter(NORM_DATETIME_PATTERN); + + /** + * 标准日期时间格式,精确到毫秒:yyyy-MM-dd HH:mm:ss.SSS + */ + public static final String NORM_DATETIME_MS_PATTERN = "yyyy-MM-dd HH:mm:ss.SSS"; + /** + * 标准日期时间格式,精确到毫秒 {@link FastDateFormat}:yyyy-MM-dd HH:mm:ss.SSS + */ + public static final FastDateFormat NORM_DATETIME_MS_FORMAT = FastDateFormat.getInstance(NORM_DATETIME_MS_PATTERN); + /** + * 标准日期时间格式,精确到毫秒 {@link FastDateFormat}:yyyy-MM-dd HH:mm:ss.SSS + */ + public static final DateTimeFormatter NORM_DATETIME_MS_FORMATTER = createFormatter(NORM_DATETIME_MS_PATTERN); + + /** + * ISO8601日期时间格式,精确到毫秒:yyyy-MM-dd HH:mm:ss,SSS + */ + public static final String ISO8601_PATTERN = "yyyy-MM-dd HH:mm:ss,SSS"; + /** + * ISO8601日期时间格式,精确到毫秒 {@link FastDateFormat}:yyyy-MM-dd HH:mm:ss,SSS + */ + public static final FastDateFormat ISO8601_FORMAT = FastDateFormat.getInstance(ISO8601_PATTERN); + /** + * 标准日期格式 {@link FastDateFormat}:yyyy-MM-dd HH:mm:ss,SSS + */ + public static final DateTimeFormatter ISO8601_FORMATTER = createFormatter(ISO8601_PATTERN); + + /** + * 标准日期格式:yyyy年MM月dd日 + */ + public static final String CHINESE_DATE_PATTERN = "yyyy年MM月dd日"; + /** + * 标准日期格式 {@link FastDateFormat}:yyyy年MM月dd日 + */ + public static final FastDateFormat CHINESE_DATE_FORMAT = FastDateFormat.getInstance(CHINESE_DATE_PATTERN); + /** + * 标准日期格式 {@link FastDateFormat}:yyyy年MM月dd日 + */ + public static final DateTimeFormatter CHINESE_DATE_FORMATTER = createFormatter(CHINESE_DATE_PATTERN); + + /** + * 标准日期格式:yyyy年MM月dd日 HH时mm分ss秒 + */ + public static final String CHINESE_DATE_TIME_PATTERN = "yyyy年MM月dd日HH时mm分ss秒"; + /** + * 标准日期格式 {@link FastDateFormat}:yyyy年MM月dd日HH时mm分ss秒 + */ + public static final FastDateFormat CHINESE_DATE_TIME_FORMAT = FastDateFormat.getInstance(CHINESE_DATE_TIME_PATTERN); + /** + * 标准日期格式 {@link FastDateFormat}:yyyy年MM月dd日HH时mm分ss秒 + */ + public static final DateTimeFormatter CHINESE_DATE_TIME_FORMATTER = createFormatter(CHINESE_DATE_TIME_PATTERN); + + //-------------------------------------------------------------------------------------------------------------------------------- Pure + /** + * 标准日期格式:yyyyMMdd + */ + public static final String PURE_DATE_PATTERN = "yyyyMMdd"; + /** + * 标准日期格式 {@link FastDateFormat}:yyyyMMdd + */ + public static final FastDateFormat PURE_DATE_FORMAT = FastDateFormat.getInstance(PURE_DATE_PATTERN); + /** + * 标准日期格式 {@link FastDateFormat}:yyyyMMdd + */ + public static final DateTimeFormatter PURE_DATE_FORMATTER = createFormatter(PURE_DATE_PATTERN); + + /** + * 标准日期格式:HHmmss + */ + public static final String PURE_TIME_PATTERN = "HHmmss"; + /** + * 标准日期格式 {@link FastDateFormat}:HHmmss + */ + public static final FastDateFormat PURE_TIME_FORMAT = FastDateFormat.getInstance(PURE_TIME_PATTERN); + /** + * 标准日期格式 {@link FastDateFormat}:HHmmss + */ + public static final DateTimeFormatter PURE_TIME_FORMATTER = createFormatter(PURE_TIME_PATTERN); + + /** + * 标准日期格式:yyyyMMddHHmmss + */ + public static final String PURE_DATETIME_PATTERN = "yyyyMMddHHmmss"; + /** + * 标准日期格式 {@link FastDateFormat}:yyyyMMddHHmmss + */ + public static final FastDateFormat PURE_DATETIME_FORMAT = FastDateFormat.getInstance(PURE_DATETIME_PATTERN); + /** + * 标准日期格式 {@link FastDateFormat}:yyyyMMddHHmmss + */ + public static final DateTimeFormatter PURE_DATETIME_FORMATTER = createFormatter(PURE_DATETIME_PATTERN); + + /** + * 标准日期格式:yyyyMMddHHmmssSSS + */ + public static final String PURE_DATETIME_MS_PATTERN = "yyyyMMddHHmmssSSS"; + /** + * 标准日期格式 {@link FastDateFormat}:yyyyMMddHHmmssSSS + */ + public static final FastDateFormat PURE_DATETIME_MS_FORMAT = FastDateFormat.getInstance(PURE_DATETIME_MS_PATTERN); + /** + * 标准日期格式 {@link FastDateFormat}:yyyyMMddHHmmssSSS + */ + public static final DateTimeFormatter PURE_DATETIME_MS_FORMATTER = createFormatter(PURE_DATETIME_MS_PATTERN); + + //-------------------------------------------------------------------------------------------------------------------------------- Others + /** + * HTTP头中日期时间格式:EEE, dd MMM yyyy HH:mm:ss z + */ + public static final String HTTP_DATETIME_PATTERN = "EEE, dd MMM yyyy HH:mm:ss z"; + /** + * HTTP头中日期时间格式 {@link FastDateFormat}:EEE, dd MMM yyyy HH:mm:ss z + */ + public static final FastDateFormat HTTP_DATETIME_FORMAT = FastDateFormat.getInstance(HTTP_DATETIME_PATTERN, TimeZone.getTimeZone("GMT"), Locale.US); + + /** + * JDK中日期时间格式:EEE MMM dd HH:mm:ss zzz yyyy + */ + public static final String JDK_DATETIME_PATTERN = "EEE MMM dd HH:mm:ss zzz yyyy"; + /** + * JDK中日期时间格式 {@link FastDateFormat}:EEE MMM dd HH:mm:ss zzz yyyy + */ + public static final FastDateFormat JDK_DATETIME_FORMAT = FastDateFormat.getInstance(JDK_DATETIME_PATTERN, Locale.US); + + /** + * UTC时间:yyyy-MM-dd'T'HH:mm:ss + */ + public static final String UTC_SIMPLE_PATTERN = "yyyy-MM-dd'T'HH:mm:ss"; + /** + * UTC时间{@link FastDateFormat}:yyyy-MM-dd'T'HH:mm:ss + */ + public static final FastDateFormat UTC_SIMPLE_FORMAT = FastDateFormat.getInstance(UTC_SIMPLE_PATTERN, TimeZone.getTimeZone("UTC")); + + /** + * UTC时间:yyyy-MM-dd'T'HH:mm:ss.SSS + */ + public static final String UTC_SIMPLE_MS_PATTERN = "yyyy-MM-dd'T'HH:mm:ss.SSS"; + /** + * UTC时间{@link FastDateFormat}:yyyy-MM-dd'T'HH:mm:ss.SSS + */ + public static final FastDateFormat UTC_SIMPLE_MS_FORMAT = FastDateFormat.getInstance(UTC_SIMPLE_MS_PATTERN, TimeZone.getTimeZone("UTC")); + + /** + * UTC时间:yyyy-MM-dd'T'HH:mm:ss'Z' + */ + public static final String UTC_PATTERN = "yyyy-MM-dd'T'HH:mm:ss'Z'"; + /** + * UTC时间{@link FastDateFormat}:yyyy-MM-dd'T'HH:mm:ss'Z' + */ + public static final FastDateFormat UTC_FORMAT = FastDateFormat.getInstance(UTC_PATTERN, TimeZone.getTimeZone("UTC")); + + /** + * UTC时间:yyyy-MM-dd'T'HH:mm:ssZ + */ + public static final String UTC_WITH_ZONE_OFFSET_PATTERN = "yyyy-MM-dd'T'HH:mm:ssZ"; + /** + * UTC时间{@link FastDateFormat}:yyyy-MM-dd'T'HH:mm:ssZ + */ + public static final FastDateFormat UTC_WITH_ZONE_OFFSET_FORMAT = FastDateFormat.getInstance(UTC_WITH_ZONE_OFFSET_PATTERN, TimeZone.getTimeZone("UTC")); + + /** + * UTC时间:yyyy-MM-dd'T'HH:mm:ssXXX + */ + public static final String UTC_WITH_XXX_OFFSET_PATTERN = "yyyy-MM-dd'T'HH:mm:ssXXX"; + /** + * UTC时间{@link FastDateFormat}:yyyy-MM-dd'T'HH:mm:ssXXX + */ + public static final FastDateFormat UTC_WITH_XXX_OFFSET_FORMAT = FastDateFormat.getInstance(UTC_WITH_XXX_OFFSET_PATTERN); + + /** + * UTC时间:yyyy-MM-dd'T'HH:mm:ss.SSS'Z' + */ + public static final String UTC_MS_PATTERN = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"; + /** + * UTC时间{@link FastDateFormat}:yyyy-MM-dd'T'HH:mm:ss.SSS'Z' + */ + public static final FastDateFormat UTC_MS_FORMAT = FastDateFormat.getInstance(UTC_MS_PATTERN, TimeZone.getTimeZone("UTC")); + + /** + * UTC时间:yyyy-MM-dd'T'HH:mm:ssZ + */ + public static final String UTC_MS_WITH_ZONE_OFFSET_PATTERN = "yyyy-MM-dd'T'HH:mm:ss.SSSZ"; + /** + * UTC时间{@link FastDateFormat}:yyyy-MM-dd'T'HH:mm:ssZ + */ + public static final FastDateFormat UTC_MS_WITH_ZONE_OFFSET_FORMAT = FastDateFormat.getInstance(UTC_MS_WITH_ZONE_OFFSET_PATTERN, TimeZone.getTimeZone("UTC")); + + /** + * UTC时间:yyyy-MM-dd'T'HH:mm:ss.SSSXXX + */ + public static final String UTC_MS_WITH_XXX_OFFSET_PATTERN = "yyyy-MM-dd'T'HH:mm:ss.SSSXXX"; + /** + * UTC时间{@link FastDateFormat}:yyyy-MM-dd'T'HH:mm:ss.SSSXXX + */ + public static final FastDateFormat UTC_MS_WITH_XXX_OFFSET_FORMAT = FastDateFormat.getInstance(UTC_MS_WITH_XXX_OFFSET_PATTERN); + + /** + * 创建并为 {@link DateTimeFormatter} 赋予默认时区和位置信息,默认值为系统默认值。 + * + * @param pattern 日期格式 + * @return {@link DateTimeFormatter} + * @since 5.7.5 + */ + public static DateTimeFormatter createFormatter(String pattern) { + return DateTimeFormatter.ofPattern(pattern, Locale.getDefault()) + .withZone(ZoneId.systemDefault()); + } +} diff --git a/src/main/java/cn/hutool/core/date/DateRange.java b/src/main/java/cn/hutool/core/date/DateRange.java new file mode 100644 index 0000000..0e91132 --- /dev/null +++ b/src/main/java/cn/hutool/core/date/DateRange.java @@ -0,0 +1,59 @@ +package cn.hutool.core.date; + +import cn.hutool.core.lang.Range; + +import java.util.Date; + +/** + * 日期范围 + * + * @author looly + * @since 4.1.0 + */ +public class DateRange extends Range { + private static final long serialVersionUID = 1L; + + /** + * 构造,包含开始和结束日期时间 + * + * @param start 起始日期时间(包括) + * @param end 结束日期时间(包括) + * @param unit 步进单位 + */ + public DateRange(Date start, Date end, DateField unit) { + this(start, end, unit, 1); + } + + /** + * 构造,包含开始和结束日期时间 + * + * @param start 起始日期时间(包括) + * @param end 结束日期时间(包括) + * @param unit 步进单位 + * @param step 步进数 + */ + public DateRange(Date start, Date end, DateField unit, int step) { + this(start, end, unit, step, true, true); + } + + /** + * 构造 + * + * @param start 起始日期时间 + * @param end 结束日期时间 + * @param unit 步进单位 + * @param step 步进数 + * @param isIncludeStart 是否包含开始的时间 + * @param isIncludeEnd 是否包含结束的时间 + */ + public DateRange(Date start, Date end, DateField unit, int step, boolean isIncludeStart, boolean isIncludeEnd) { + super(DateUtil.date(start), DateUtil.date(end), (current, end1, index) -> { + final DateTime dt = DateUtil.date(start).offsetNew(unit, (index + 1) * step); + if (dt.isAfter(end1)) { + return null; + } + return dt; + }, isIncludeStart, isIncludeEnd); + } + +} diff --git a/src/main/java/cn/hutool/core/date/DateTime.java b/src/main/java/cn/hutool/core/date/DateTime.java new file mode 100644 index 0000000..facdff4 --- /dev/null +++ b/src/main/java/cn/hutool/core/date/DateTime.java @@ -0,0 +1,1113 @@ +package cn.hutool.core.date; + +import cn.hutool.core.date.format.DateParser; +import cn.hutool.core.date.format.DatePrinter; +import cn.hutool.core.date.format.FastDateFormat; +import cn.hutool.core.date.format.GlobalCustomFormat; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.core.util.SystemPropsUtil; + +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.time.temporal.TemporalAccessor; +import java.util.Calendar; +import java.util.Date; +import java.util.Locale; +import java.util.TimeZone; + +/** + * 包装{@link Date}
+ * 此类继承了{@link Date},并提供扩展方法,如时区等。
+ * 此类重写了父类的{@code toString()}方法,返回值为"yyyy-MM-dd HH:mm:ss"格式 + * + * @author xiaoleilu + */ +public class DateTime extends Date { + private static final long serialVersionUID = -5395712593979185936L; + + private static boolean useJdkToStringStyle = false; + + /** + * 设置全局的,是否使用{@link Date}默认的toString()格式
+ * 如果为{@code true},则调用toString()时返回"EEE MMM dd HH:mm:ss zzz yyyy"格式,
+ * 如果为{@code false},则返回"yyyy-MM-dd HH:mm:ss",
+ * 默认为{@code false} + * + * @param customUseJdkToStringStyle 是否使用{@link Date}默认的toString()格式 + * @since 5.7.21 + */ + public static void setUseJdkToStringStyle(boolean customUseJdkToStringStyle){ + useJdkToStringStyle = customUseJdkToStringStyle; + } + + /** + * 是否可变对象 + */ + private boolean mutable = true; + /** + * 一周的第一天,默认是周一, 在设置或获得 WEEK_OF_MONTH 或 WEEK_OF_YEAR 字段时,Calendar 必须确定一个月或一年的第一个星期,以此作为参考点。 + */ + private Week firstDayOfWeek = Week.MONDAY; + /** + * 时区 + */ + private TimeZone timeZone; + + /** + * 第一周最少天数 + */ + private int minimalDaysInFirstWeek; + + /** + * 转换时间戳为 DateTime + * + * @param timeMillis 时间戳,毫秒数 + * @return DateTime + * @since 4.6.3 + */ + public static DateTime of(long timeMillis) { + return new DateTime(timeMillis); + } + + /** + * 转换JDK date为 DateTime + * + * @param date JDK Date + * @return DateTime + */ + public static DateTime of(Date date) { + if (date instanceof DateTime) { + return (DateTime) date; + } + return new DateTime(date); + } + + /** + * 转换 {@link Calendar} 为 DateTime + * + * @param calendar {@link Calendar} + * @return DateTime + */ + public static DateTime of(Calendar calendar) { + return new DateTime(calendar); + } + + /** + * 构造 + * + * @param dateStr Date字符串 + * @param format 格式 + * @return this + * @see DatePattern + */ + public static DateTime of(String dateStr, String format) { + return new DateTime(dateStr, format); + } + + /** + * 现在的时间 + * + * @return 现在的时间 + */ + public static DateTime now() { + return new DateTime(); + } + + // -------------------------------------------------------------------- Constructor start + + /** + * 当前时间 + */ + public DateTime() { + this(TimeZone.getDefault()); + } + + /** + * 当前时间 + * + * @param timeZone 时区 + * @since 4.1.2 + */ + public DateTime(TimeZone timeZone) { + this(System.currentTimeMillis(), timeZone); + } + + /** + * 给定日期的构造 + * + * @param date 日期 + */ + public DateTime(Date date) { + this( + date,// + (date instanceof DateTime) ? ((DateTime) date).timeZone : TimeZone.getDefault() + ); + } + + /** + * 给定日期的构造 + * + * @param date 日期 + * @param timeZone 时区 + * @since 4.1.2 + */ + public DateTime(Date date, TimeZone timeZone) { + this(ObjectUtil.defaultIfNull(date, new Date()).getTime(), timeZone); + } + + /** + * 给定日期的构造 + * + * @param calendar {@link Calendar} + */ + public DateTime(Calendar calendar) { + this(calendar.getTime(), calendar.getTimeZone()); + this.setFirstDayOfWeek(Week.of(calendar.getFirstDayOfWeek())); + } + + /** + * 给定日期Instant的构造 + * + * @param instant {@link Instant} 对象 + * @since 5.0.0 + */ + public DateTime(Instant instant) { + this(instant.toEpochMilli()); + } + + /** + * 给定日期Instant的构造 + * + * @param instant {@link Instant} 对象 + * @param zoneId 时区ID + * @since 5.0.5 + */ + public DateTime(Instant instant, ZoneId zoneId) { + this(instant.toEpochMilli(), ZoneUtil.toTimeZone(zoneId)); + } + + /** + * 给定日期TemporalAccessor的构造 + * + * @param temporalAccessor {@link TemporalAccessor} 对象 + * @since 5.0.0 + */ + public DateTime(TemporalAccessor temporalAccessor) { + this(TemporalAccessorUtil.toInstant(temporalAccessor)); + } + + /** + * 给定日期ZonedDateTime的构造 + * + * @param zonedDateTime {@link ZonedDateTime} 对象 + * @since 5.0.5 + */ + public DateTime(ZonedDateTime zonedDateTime) { + this(zonedDateTime.toInstant(), zonedDateTime.getZone()); + } + + /** + * 给定日期毫秒数的构造 + * + * @param timeMillis 日期毫秒数 + * @since 4.1.2 + */ + public DateTime(long timeMillis) { + this(timeMillis, TimeZone.getDefault()); + } + + /** + * 给定日期毫秒数的构造 + * + * @param timeMillis 日期毫秒数 + * @param timeZone 时区 + * @since 4.1.2 + */ + public DateTime(long timeMillis, TimeZone timeZone) { + super(timeMillis); + this.timeZone = ObjectUtil.defaultIfNull(timeZone, TimeZone::getDefault); + } + + /** + * 构造格式:
+ *
    + *
  1. yyyy-MM-dd HH:mm:ss
  2. + *
  3. yyyy/MM/dd HH:mm:ss
  4. + *
  5. yyyy.MM.dd HH:mm:ss
  6. + *
  7. yyyy年MM月dd日 HH时mm分ss秒
  8. + *
  9. yyyy-MM-dd
  10. + *
  11. yyyy/MM/dd
  12. + *
  13. yyyy.MM.dd
  14. + *
  15. HH:mm:ss
  16. + *
  17. HH时mm分ss秒
  18. + *
  19. yyyy-MM-dd HH:mm
  20. + *
  21. yyyy-MM-dd HH:mm:ss.SSS
  22. + *
  23. yyyyMMddHHmmss
  24. + *
  25. yyyyMMddHHmmssSSS
  26. + *
  27. yyyyMMdd
  28. + *
  29. EEE, dd MMM yyyy HH:mm:ss z
  30. + *
  31. EEE MMM dd HH:mm:ss zzz yyyy
  32. + *
  33. yyyy-MM-dd'T'HH:mm:ss'Z'
  34. + *
  35. yyyy-MM-dd'T'HH:mm:ss.SSS'Z'
  36. + *
  37. yyyy-MM-dd'T'HH:mm:ssZ
  38. + *
  39. yyyy-MM-dd'T'HH:mm:ss.SSSZ
  40. + *
+ * + * @param dateStr Date字符串 + * @since 5.6.2 + */ + public DateTime(CharSequence dateStr) { + this(DateUtil.parse(dateStr)); + } + + /** + * 构造 + * + * @param dateStr Date字符串 + * @param format 格式 + * @see DatePattern + */ + public DateTime(CharSequence dateStr, String format) { + this(GlobalCustomFormat.isCustomFormat(format) + ? GlobalCustomFormat.parse(dateStr, format) + : parse(dateStr, DateUtil.newSimpleFormat(format))); + } + + /** + * 构造 + * + * @param dateStr Date字符串 + * @param dateFormat 格式化器 {@link SimpleDateFormat} + * @see DatePattern + */ + public DateTime(CharSequence dateStr, DateFormat dateFormat) { + this(parse(dateStr, dateFormat), dateFormat.getTimeZone()); + } + + /** + * 构建DateTime对象 + * + * @param dateStr Date字符串 + * @param formatter 格式化器,{@link DateTimeFormatter} + * @since 5.0.0 + */ + public DateTime(CharSequence dateStr, DateTimeFormatter formatter) { + this(TemporalAccessorUtil.toInstant(formatter.parse(dateStr)), formatter.getZone()); + } + + /** + * 构造 + * + * @param dateStr Date字符串 + * @param dateParser 格式化器 {@link DateParser},可以使用 {@link FastDateFormat} + * @see DatePattern + */ + public DateTime(CharSequence dateStr, DateParser dateParser) { + this(dateStr, dateParser, SystemPropsUtil.getBoolean(SystemPropsUtil.HUTOOL_DATE_LENIENT, true)); + } + + /** + * 构造 + * + * @param dateStr Date字符串 + * @param dateParser 格式化器 {@link DateParser},可以使用 {@link FastDateFormat} + * @param lenient 是否宽容模式 + * @see DatePattern + */ + public DateTime(CharSequence dateStr, DateParser dateParser, boolean lenient) { + this(parse(dateStr, dateParser, lenient)); + } + + // -------------------------------------------------------------------- Constructor end + + // -------------------------------------------------------------------- offset start + + /** + * 调整日期和时间
+ * 如果此对象为可变对象,返回自身,否则返回新对象,设置是否可变对象见{@link #setMutable(boolean)} + * + * @param datePart 调整的部分 {@link DateField} + * @param offset 偏移量,正数为向后偏移,负数为向前偏移 + * @return 如果此对象为可变对象,返回自身,否则返回新对象 + */ + public DateTime offset(DateField datePart, int offset) { + if (DateField.ERA == datePart) { + throw new IllegalArgumentException("ERA is not support offset!"); + } + + final Calendar cal = toCalendar(); + //noinspection MagicConstant + cal.add(datePart.getValue(), offset); + + DateTime dt = mutable ? this : ObjectUtil.clone(this); + return dt.setTimeInternal(cal.getTimeInMillis()); + } + + /** + * 调整日期和时间
+ * 返回调整后的新DateTime,不影响原对象 + * + * @param datePart 调整的部分 {@link DateField} + * @param offset 偏移量,正数为向后偏移,负数为向前偏移 + * @return 如果此对象为可变对象,返回自身,否则返回新对象 + * @since 3.0.9 + */ + public DateTime offsetNew(DateField datePart, int offset) { + final Calendar cal = toCalendar(); + //noinspection MagicConstant + cal.add(datePart.getValue(), offset); + + return ObjectUtil.clone(this).setTimeInternal(cal.getTimeInMillis()); + } + // -------------------------------------------------------------------- offset end + + // -------------------------------------------------------------------- Part of Date start + + /** + * 获得日期的某个部分
+ * 例如获得年的部分,则使用 getField(DatePart.YEAR) + * + * @param field 表示日期的哪个部分的枚举 {@link DateField} + * @return 某个部分的值 + */ + public int getField(DateField field) { + return getField(field.getValue()); + } + + /** + * 获得日期的某个部分
+ * 例如获得年的部分,则使用 getField(Calendar.YEAR) + * + * @param field 表示日期的哪个部分的int值 {@link Calendar} + * @return 某个部分的值 + */ + public int getField(int field) { + return toCalendar().get(field); + } + + /** + * 设置日期的某个部分
+ * 如果此对象为可变对象,返回自身,否则返回新对象,设置是否可变对象见{@link #setMutable(boolean)} + * + * @param field 表示日期的哪个部分的枚举 {@link DateField} + * @param value 值 + * @return this + */ + public DateTime setField(DateField field, int value) { + return setField(field.getValue(), value); + } + + /** + * 设置日期的某个部分
+ * 如果此对象为可变对象,返回自身,否则返回新对象,设置是否可变对象见{@link #setMutable(boolean)} + * + * @param field 表示日期的哪个部分的int值 {@link Calendar} + * @param value 值 + * @return this + */ + public DateTime setField(int field, int value) { + final Calendar calendar = toCalendar(); + calendar.set(field, value); + + DateTime dt = this; + if (!mutable) { + dt = ObjectUtil.clone(this); + } + return dt.setTimeInternal(calendar.getTimeInMillis()); + } + + @Override + public void setTime(long time) { + if (mutable) { + super.setTime(time); + } else { + throw new DateException("This is not a mutable object !"); + } + } + + /** + * 获得年的部分 + * + * @return 年的部分 + */ + public int year() { + return getField(DateField.YEAR); + } + + /** + * 获得当前日期所属季度,从1开始计数
+ * + * @return 第几个季度 {@link Quarter} + */ + public int quarter() { + return month() / 3 + 1; + } + + /** + * 获得当前日期所属季度
+ * + * @return 第几个季度 {@link Quarter} + */ + public Quarter quarterEnum() { + return Quarter.of(quarter()); + } + + /** + * 获得月份,从0开始计数 + * + * @return 月份 + */ + public int month() { + return getField(DateField.MONTH); + } + + /** + * 获取月,从1开始计数 + * + * @return 月份,1表示一月 + * @since 5.4.1 + */ + public int monthBaseOne() { + return month() + 1; + } + + /** + * 获得月份,从1开始计数
+ * 由于{@link Calendar} 中的月份按照0开始计数,导致某些需求容易误解,因此如果想用1表示一月,2表示二月则调用此方法 + * + * @return 月份 + */ + public int monthStartFromOne() { + return month() + 1; + } + + /** + * 获得月份 + * + * @return {@link Month} + */ + public Month monthEnum() { + return Month.of(month()); + } + + /** + * 获得指定日期是所在年份的第几周
+ * 此方法返回值与一周的第一天有关,比如:
+ * 2016年1月3日为周日,如果一周的第一天为周日,那这天是第二周(返回2)
+ * 如果一周的第一天为周一,那这天是第一周(返回1)
+ * 跨年的那个星期得到的结果总是1 + * + * @return 周 + * @see #setFirstDayOfWeek(Week) + */ + public int weekOfYear() { + return getField(DateField.WEEK_OF_YEAR); + } + + /** + * 获得指定日期是所在月份的第几周
+ * 此方法返回值与一周的第一天有关,比如:
+ * 2016年1月3日为周日,如果一周的第一天为周日,那这天是第二周(返回2)
+ * 如果一周的第一天为周一,那这天是第一周(返回1) + * + * @return 周 + * @see #setFirstDayOfWeek(Week) + */ + public int weekOfMonth() { + return getField(DateField.WEEK_OF_MONTH); + } + + /** + * 获得指定日期是这个日期所在月份的第几天,从1开始 + * + * @return 天,1表示第一天 + */ + public int dayOfMonth() { + return getField(DateField.DAY_OF_MONTH); + } + + /** + * 获得指定日期是这个日期所在年份的第几天,从1开始 + * + * @return 天,1表示第一天 + * @since 5.3.6 + */ + public int dayOfYear() { + return getField(DateField.DAY_OF_YEAR); + } + + /** + * 获得指定日期是星期几,1表示周日,2表示周一 + * + * @return 星期几 + */ + public int dayOfWeek() { + return getField(DateField.DAY_OF_WEEK); + } + + /** + * 获得天所在的周是这个月的第几周 + * + * @return 天 + */ + public int dayOfWeekInMonth() { + return getField(DateField.DAY_OF_WEEK_IN_MONTH); + } + + /** + * 获得指定日期是星期几 + * + * @return {@link Week} + */ + public Week dayOfWeekEnum() { + return Week.of(dayOfWeek()); + } + + /** + * 获得指定日期的小时数部分
+ * + * @param is24HourClock 是否24小时制 + * @return 小时数 + */ + public int hour(boolean is24HourClock) { + return getField(is24HourClock ? DateField.HOUR_OF_DAY : DateField.HOUR); + } + + /** + * 获得指定日期的分钟数部分
+ * 例如:10:04:15.250 =》 4 + * + * @return 分钟数 + */ + public int minute() { + return getField(DateField.MINUTE); + } + + /** + * 获得指定日期的秒数部分
+ * + * @return 秒数 + */ + public int second() { + return getField(DateField.SECOND); + } + + /** + * 获得指定日期的毫秒数部分
+ * + * @return 毫秒数 + */ + public int millisecond() { + return getField(DateField.MILLISECOND); + } + + /** + * 是否为上午 + * + * @return 是否为上午 + */ + public boolean isAM() { + return Calendar.AM == getField(DateField.AM_PM); + } + + /** + * 是否为下午 + * + * @return 是否为下午 + */ + public boolean isPM() { + return Calendar.PM == getField(DateField.AM_PM); + } + + /** + * 是否为周末,周末指周六或者周日 + * + * @return 是否为周末,周末指周六或者周日 + * @since 4.1.14 + */ + public boolean isWeekend() { + final int dayOfWeek = dayOfWeek(); + return Calendar.SATURDAY == dayOfWeek || Calendar.SUNDAY == dayOfWeek; + } + // -------------------------------------------------------------------- Part of Date end + + /** + * 是否闰年 + * + * @return 是否闰年 + * @see DateUtil#isLeapYear(int) + */ + public boolean isLeapYear() { + return DateUtil.isLeapYear(year()); + } + + /** + * 转换为Calendar, 默认 {@link Locale} + * + * @return {@link Calendar} + */ + public Calendar toCalendar() { + return toCalendar(Locale.getDefault(Locale.Category.FORMAT)); + } + + /** + * 转换为Calendar + * + * @param locale 地域 {@link Locale} + * @return {@link Calendar} + */ + public Calendar toCalendar(Locale locale) { + return toCalendar(this.timeZone, locale); + } + + /** + * 转换为Calendar + * + * @param zone 时区 {@link TimeZone} + * @return {@link Calendar} + */ + public Calendar toCalendar(TimeZone zone) { + return toCalendar(zone, Locale.getDefault(Locale.Category.FORMAT)); + } + + /** + * 转换为Calendar + * + * @param zone 时区 {@link TimeZone} + * @param locale 地域 {@link Locale} + * @return {@link Calendar} + */ + public Calendar toCalendar(TimeZone zone, Locale locale) { + if (null == locale) { + locale = Locale.getDefault(Locale.Category.FORMAT); + } + final Calendar cal = (null != zone) ? Calendar.getInstance(zone, locale) : Calendar.getInstance(locale); + //noinspection MagicConstant + cal.setFirstDayOfWeek(firstDayOfWeek.getValue()); + // issue#1988@Github + if (minimalDaysInFirstWeek > 0) { + cal.setMinimalDaysInFirstWeek(minimalDaysInFirstWeek); + } + cal.setTime(this); + return cal; + } + + /** + * 转换为 {@link Date}
+ * 考虑到很多框架(例如Hibernate)的兼容性,提供此方法返回JDK原生的Date对象 + * + * @return {@link Date} + * @since 3.2.2 + */ + public Date toJdkDate() { + return new Date(this.getTime()); + } + + /** + * 转换为 {@link LocalDateTime} + * + * @return {@link LocalDateTime} + * @since 5.7.16 + */ + public LocalDateTime toLocalDateTime() { + return LocalDateTimeUtil.of(this); + } + + /** + * 计算相差时长 + * + * @param date 对比的日期 + * @return {@link DateBetween} + */ + public DateBetween between(Date date) { + return new DateBetween(this, date); + } + + /** + * 计算相差时长 + * + * @param date 对比的日期 + * @param unit 单位 {@link DateUnit} + * @return 相差时长 + */ + public long between(Date date, DateUnit unit) { + return new DateBetween(this, date).between(unit); + } + + /** + * 计算相差时长 + * + * @param date 对比的日期 + * @param unit 单位 {@link DateUnit} + * @param formatLevel 格式化级别 + * @return 相差时长 + */ + public String between(Date date, DateUnit unit, BetweenFormatter.Level formatLevel) { + return new DateBetween(this, date).toString(unit, formatLevel); + } + + /** + * 当前日期是否在日期指定范围内
+ * 起始日期和结束日期可以互换 + * + * @param beginDate 起始日期(包含) + * @param endDate 结束日期(包含) + * @return 是否在范围内 + * @since 3.0.8 + */ + public boolean isIn(Date beginDate, Date endDate) { + long beginMills = beginDate.getTime(); + long endMills = endDate.getTime(); + long thisMills = this.getTime(); + + return thisMills >= Math.min(beginMills, endMills) && thisMills <= Math.max(beginMills, endMills); + } + + /** + * 是否在给定日期之前 + * + * @param date 日期 + * @return 是否在给定日期之前 + * @since 4.1.3 + */ + public boolean isBefore(Date date) { + if (null == date) { + throw new NullPointerException("Date to compare is null !"); + } + return compareTo(date) < 0; + } + + /** + * 是否在给定日期之前或与给定日期相等 + * + * @param date 日期 + * @return 是否在给定日期之前或与给定日期相等 + * @since 3.0.9 + */ + public boolean isBeforeOrEquals(Date date) { + if (null == date) { + throw new NullPointerException("Date to compare is null !"); + } + return compareTo(date) <= 0; + } + + /** + * 是否在给定日期之后 + * + * @param date 日期 + * @return 是否在给定日期之后 + * @since 4.1.3 + */ + public boolean isAfter(Date date) { + if (null == date) { + throw new NullPointerException("Date to compare is null !"); + } + return compareTo(date) > 0; + } + + /** + * 是否在给定日期之后或与给定日期相等 + * + * @param date 日期 + * @return 是否在给定日期之后或与给定日期相等 + * @since 3.0.9 + */ + public boolean isAfterOrEquals(Date date) { + if (null == date) { + throw new NullPointerException("Date to compare is null !"); + } + return compareTo(date) >= 0; + } + + /** + * 对象是否可变
+ * 如果为不可变对象,以下方法将返回新方法: + *
    + *
  • {@link DateTime#offset(DateField, int)}
  • + *
  • {@link DateTime#setField(DateField, int)}
  • + *
  • {@link DateTime#setField(int, int)}
  • + *
+ * 如果为不可变对象,{@link DateTime#setTime(long)}将抛出异常 + * + * @return 对象是否可变 + */ + public boolean isMutable() { + return mutable; + } + + /** + * 设置对象是否可变 如果为不可变对象,以下方法将返回新方法: + *
    + *
  • {@link DateTime#offset(DateField, int)}
  • + *
  • {@link DateTime#setField(DateField, int)}
  • + *
  • {@link DateTime#setField(int, int)}
  • + *
+ * 如果为不可变对象,{@link DateTime#setTime(long)}将抛出异常 + * + * @param mutable 是否可变 + * @return this + */ + public DateTime setMutable(boolean mutable) { + this.mutable = mutable; + return this; + } + + /** + * 获得一周的第一天,默认为周一 + * + * @return 一周的第一天 + */ + public Week getFirstDayOfWeek() { + return firstDayOfWeek; + } + + /** + * 设置一周的第一天
+ * JDK的Calendar中默认一周的第一天是周日,Hutool中将此默认值设置为周一
+ * 设置一周的第一天主要影响{@link #weekOfMonth()}和{@link #weekOfYear()} 两个方法 + * + * @param firstDayOfWeek 一周的第一天 + * @return this + * @see #weekOfMonth() + * @see #weekOfYear() + */ + public DateTime setFirstDayOfWeek(Week firstDayOfWeek) { + this.firstDayOfWeek = firstDayOfWeek; + return this; + } + + /** + * 获取时区 + * + * @return 时区 + * @since 5.0.5 + */ + public TimeZone getTimeZone() { + return this.timeZone; + } + + /** + * 获取时区ID + * + * @return 时区ID + * @since 5.0.5 + */ + public ZoneId getZoneId() { + return this.timeZone.toZoneId(); + } + + /** + * 设置时区 + * + * @param timeZone 时区 + * @return this + * @since 4.1.2 + */ + public DateTime setTimeZone(TimeZone timeZone) { + this.timeZone = ObjectUtil.defaultIfNull(timeZone, TimeZone::getDefault); + return this; + } + + /** + * 设置第一周最少天数 + * + * @param minimalDaysInFirstWeek 第一周最少天数 + * @return this + * @since 5.7.17 + */ + public DateTime setMinimalDaysInFirstWeek(int minimalDaysInFirstWeek) { + this.minimalDaysInFirstWeek = minimalDaysInFirstWeek; + return this; + } + + /** + * 是否为本月最后一天 + * @return 是否为本月最后一天 + * @since 5.8.9 + */ + public boolean isLastDayOfMonth(){ + return dayOfMonth() == getLastDayOfMonth(); + } + + /** + * 获得本月的最后一天 + * @return 天 + * @since 5.8.9 + */ + public int getLastDayOfMonth(){ + return monthEnum().getLastDay(isLeapYear()); + } + + // -------------------------------------------------------------------- toString start + + /** + * 转为字符串,如果时区被设置,会转换为其时区对应的时间,否则转换为当前地点对应的时区
+ * 可以调用{@link DateTime#setUseJdkToStringStyle(boolean)} 方法自定义默认的风格
+ * 如果{@link #useJdkToStringStyle}为{@code true},返回"EEE MMM dd HH:mm:ss zzz yyyy"格式,
+ * 如果为{@code false},则返回"yyyy-MM-dd HH:mm:ss" + * + * @return 格式字符串 + */ + @Override + public String toString() { + if(useJdkToStringStyle){ + return super.toString(); + } + return toString(this.timeZone); + } + + /** + * 转为"yyyy-MM-dd HH:mm:ss" 格式字符串
+ * 时区使用当前地区的默认时区 + * + * @return "yyyy-MM-dd HH:mm:ss" 格式字符串 + * @since 4.1.14 + */ + public String toStringDefaultTimeZone() { + return toString(TimeZone.getDefault()); + } + + /** + * 转为"yyyy-MM-dd HH:mm:ss" 格式字符串
+ * 如果时区不为{@code null},会转换为其时区对应的时间,否则转换为当前时间对应的时区 + * + * @param timeZone 时区 + * @return "yyyy-MM-dd HH:mm:ss" 格式字符串 + * @since 4.1.14 + */ + public String toString(TimeZone timeZone) { + if (null != timeZone) { + return toString(DateUtil.newSimpleFormat(DatePattern.NORM_DATETIME_PATTERN, null, timeZone)); + } + return toString(DatePattern.NORM_DATETIME_FORMAT); + } + + /** + * 转为"yyyy-MM-dd" 格式字符串 + * + * @return "yyyy-MM-dd" 格式字符串 + * @since 4.0.0 + */ + public String toDateStr() { + if (null != this.timeZone) { + return toString(DateUtil.newSimpleFormat(DatePattern.NORM_DATE_PATTERN, null, timeZone)); + } + return toString(DatePattern.NORM_DATE_FORMAT); + } + + /** + * 转为"HH:mm:ss" 格式字符串 + * + * @return "HH:mm:ss" 格式字符串 + * @since 4.1.4 + */ + public String toTimeStr() { + if (null != this.timeZone) { + return toString(DateUtil.newSimpleFormat(DatePattern.NORM_TIME_PATTERN, null, timeZone)); + } + return toString(DatePattern.NORM_TIME_FORMAT); + } + + /** + * 转为字符串 + * + * @param format 日期格式,常用格式见: {@link DatePattern} + * @return String + */ + public String toString(String format) { + if (null != this.timeZone) { + return toString(DateUtil.newSimpleFormat(format, null, timeZone)); + } + return toString(FastDateFormat.getInstance(format)); + } + + /** + * 转为字符串 + * + * @param format {@link DatePrinter} 或 {@link FastDateFormat} + * @return String + */ + public String toString(DatePrinter format) { + return format.format(this); + } + + /** + * 转为字符串 + * + * @param format {@link SimpleDateFormat} + * @return String + */ + public String toString(DateFormat format) { + return format.format(this); + } + + /** + * @return 输出精确到毫秒的标准日期形式 + */ + public String toMsStr() { + return toString(DatePattern.NORM_DATETIME_MS_FORMAT); + } + // -------------------------------------------------------------------- toString end + + /** + * 转换字符串为Date + * + * @param dateStr 日期字符串 + * @param dateFormat {@link SimpleDateFormat} + * @return {@link Date} + */ + private static Date parse(CharSequence dateStr, DateFormat dateFormat) { + Assert.notBlank(dateStr, "Date String must be not blank !"); + try { + return dateFormat.parse(dateStr.toString()); + } catch (Exception e) { + String pattern; + if (dateFormat instanceof SimpleDateFormat) { + pattern = ((SimpleDateFormat) dateFormat).toPattern(); + } else { + pattern = dateFormat.toString(); + } + throw new DateException(StrUtil.format("Parse [{}] with format [{}] error!", dateStr, pattern), e); + } + } + + /** + * 转换字符串为Date + * + * @param dateStr 日期字符串 + * @param parser {@link FastDateFormat} + * @param lenient 是否宽容模式 + * @return {@link Calendar} + */ + private static Calendar parse(CharSequence dateStr, DateParser parser, boolean lenient) { + Assert.notNull(parser, "Parser or DateFromat must be not null !"); + Assert.notBlank(dateStr, "Date String must be not blank !"); + + final Calendar calendar = CalendarUtil.parse(dateStr, lenient, parser); + if (null == calendar) { + throw new DateException("Parse [{}] with format [{}] error!", dateStr, parser.getPattern()); + } + + //noinspection MagicConstant + calendar.setFirstDayOfWeek(Week.MONDAY.getValue()); + return calendar; + } + + /** + * 设置日期时间 + * + * @param time 日期时间毫秒 + * @return this + */ + private DateTime setTimeInternal(long time) { + super.setTime(time); + return this; + } +} diff --git a/src/main/java/cn/hutool/core/date/DateUnit.java b/src/main/java/cn/hutool/core/date/DateUnit.java new file mode 100644 index 0000000..17388cb --- /dev/null +++ b/src/main/java/cn/hutool/core/date/DateUnit.java @@ -0,0 +1,108 @@ +package cn.hutool.core.date; + +import java.time.temporal.ChronoUnit; + +/** + * 日期时间单位,每个单位都是以毫秒为基数 + * + * @author Looly + */ +public enum DateUnit { + /** + * 一毫秒 + */ + MS(1), + /** + * 一秒的毫秒数 + */ + SECOND(1000), + /** + * 一分钟的毫秒数 + */ + MINUTE(SECOND.getMillis() * 60), + /** + * 一小时的毫秒数 + */ + HOUR(MINUTE.getMillis() * 60), + /** + * 一天的毫秒数 + */ + DAY(HOUR.getMillis() * 24), + /** + * 一周的毫秒数 + */ + WEEK(DAY.getMillis() * 7); + + private final long millis; + + DateUnit(long millis) { + this.millis = millis; + } + + /** + * @return 单位对应的毫秒数 + */ + public long getMillis() { + return this.millis; + } + + /** + * 单位兼容转换,将DateUnit转换为对应的{@link ChronoUnit} + * + * @return {@link ChronoUnit} + * @since 5.4.5 + */ + public ChronoUnit toChronoUnit() { + return DateUnit.toChronoUnit(this); + } + + /** + * 单位兼容转换,将{@link ChronoUnit}转换为对应的DateUnit + * + * @param unit {@link ChronoUnit} + * @return DateUnit,null表示不支持此单位 + * @since 5.4.5 + */ + public static DateUnit of(ChronoUnit unit) { + switch (unit) { + case MICROS: + return DateUnit.MS; + case SECONDS: + return DateUnit.SECOND; + case MINUTES: + return DateUnit.MINUTE; + case HOURS: + return DateUnit.HOUR; + case DAYS: + return DateUnit.DAY; + case WEEKS: + return DateUnit.WEEK; + } + return null; + } + + /** + * 单位兼容转换,将DateUnit转换为对应的{@link ChronoUnit} + * + * @param unit DateUnit + * @return {@link ChronoUnit} + * @since 5.4.5 + */ + public static ChronoUnit toChronoUnit(DateUnit unit) { + switch (unit) { + case MS: + return ChronoUnit.MICROS; + case SECOND: + return ChronoUnit.SECONDS; + case MINUTE: + return ChronoUnit.MINUTES; + case HOUR: + return ChronoUnit.HOURS; + case DAY: + return ChronoUnit.DAYS; + case WEEK: + return ChronoUnit.WEEKS; + } + return null; + } +} diff --git a/src/main/java/cn/hutool/core/date/DateUtil.java b/src/main/java/cn/hutool/core/date/DateUtil.java new file mode 100644 index 0000000..fea2aff --- /dev/null +++ b/src/main/java/cn/hutool/core/date/DateUtil.java @@ -0,0 +1,2374 @@ +package cn.hutool.core.date; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.comparator.CompareUtil; +import cn.hutool.core.date.format.DateParser; +import cn.hutool.core.date.format.DatePrinter; +import cn.hutool.core.date.format.FastDateFormat; +import cn.hutool.core.date.format.GlobalCustomFormat; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.lang.PatternPool; +import cn.hutool.core.util.CharUtil; +import cn.hutool.core.util.NumberUtil; +import cn.hutool.core.util.ReUtil; +import cn.hutool.core.util.StrUtil; + +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.Year; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.time.temporal.TemporalAccessor; +import java.util.*; +import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * 时间工具类 + * + * @author xiaoleilu + * @see LocalDateTimeUtil java8日志工具类 + * @see DatePattern 日期常用格式工具类 + */ +public class DateUtil extends CalendarUtil { + + /** + * java.util.Date EEE MMM zzz 缩写数组 + */ + private final static String[] wtb = { // + "sun", "mon", "tue", "wed", "thu", "fri", "sat", // 星期 + "jan", "feb", "mar", "apr", "may", "jun", "jul", "aug", "sep", "oct", "nov", "dec", // 月份 + "gmt", "ut", "utc", "est", "edt", "cst", "cdt", "mst", "mdt", "pst", "pdt"// 时间标准 + }; + + /** + * 当前时间,转换为{@link DateTime}对象 + * + * @return 当前时间 + */ + public static DateTime date() { + return new DateTime(); + } + + /** + * 当前时间,转换为{@link DateTime}对象,忽略毫秒部分 + * + * @return 当前时间 + * @since 4.6.2 + */ + public static DateTime dateSecond() { + return beginOfSecond(date()); + } + + /** + * {@link Date}类型时间转为{@link DateTime}
+ * 如果date本身为DateTime对象,则返回强转后的对象,否则新建一个DateTime对象 + * + * @param date Long类型Date(Unix时间戳),如果传入{@code null},返回{@code null} + * @return 时间对象 + * @since 3.0.7 + */ + public static DateTime date(Date date) { + if (date == null) { + return null; + } + if (date instanceof DateTime) { + return (DateTime) date; + } + return dateNew(date); + } + + /** + * 根据已有{@link Date} 产生新的{@link DateTime}对象 + * + * @param date Date对象,如果传入{@code null},返回{@code null} + * @return {@link DateTime}对象 + * @since 4.3.1 + */ + public static DateTime dateNew(Date date) { + if (date == null) { + return null; + } + return new DateTime(date); + } + + /** + * Long类型时间转为{@link DateTime}
+ * 只支持毫秒级别时间戳,如果需要秒级别时间戳,请自行×1000 + * + * @param date Long类型Date(Unix时间戳) + * @return 时间对象 + */ + public static DateTime date(long date) { + return new DateTime(date); + } + + /** + * {@link Calendar}类型时间转为{@link DateTime}
+ * 始终根据已有{@link Calendar} 产生新的{@link DateTime}对象 + * + * @param calendar {@link Calendar},如果传入{@code null},返回{@code null} + * @return 时间对象 + */ + public static DateTime date(Calendar calendar) { + if (calendar == null) { + return null; + } + return new DateTime(calendar); + } + + /** + * {@link TemporalAccessor}类型时间转为{@link DateTime}
+ * 始终根据已有{@link TemporalAccessor} 产生新的{@link DateTime}对象 + * + * @param temporalAccessor {@link TemporalAccessor},常用子类: {@link LocalDateTime}、 LocalDate,如果传入{@code null},返回{@code null} + * @return 时间对象 + * @since 5.0.0 + */ + public static DateTime date(TemporalAccessor temporalAccessor) { + if (temporalAccessor == null) { + return null; + } + return new DateTime(temporalAccessor); + } + + /** + * 当前时间的时间戳 + * + * @return 时间 + */ + public static long current() { + return System.currentTimeMillis(); + } + + /** + * 当前时间的时间戳(秒) + * + * @return 当前时间秒数 + * @since 4.0.0 + */ + public static long currentSeconds() { + return System.currentTimeMillis() / 1000; + } + + /** + * 当前时间,格式 yyyy-MM-dd HH:mm:ss + * + * @return 当前时间的标准形式字符串 + */ + public static String now() { + return formatDateTime(new DateTime()); + } + + /** + * 当前日期,格式 yyyy-MM-dd + * + * @return 当前日期的标准形式字符串 + */ + public static String today() { + return formatDate(new DateTime()); + } + + // -------------------------------------------------------------- Part of Date start + + /** + * 获得年的部分 + * + * @param date 日期 + * @return 年的部分 + */ + public static int year(Date date) { + return DateTime.of(date).year(); + } + + /** + * 获得指定日期所属季度,从1开始计数 + * + * @param date 日期 + * @return 第几个季度 + * @since 4.1.0 + */ + public static int quarter(Date date) { + return DateTime.of(date).quarter(); + } + + /** + * 获得指定日期所属季度 + * + * @param date 日期 + * @return 第几个季度枚举 + * @since 4.1.0 + */ + public static Quarter quarterEnum(Date date) { + return DateTime.of(date).quarterEnum(); + } + + /** + * 获得月份,从0开始计数 + * + * @param date 日期 + * @return 月份,从0开始计数 + */ + public static int month(Date date) { + return DateTime.of(date).month(); + } + + /** + * 获得月份 + * + * @param date 日期 + * @return {@link Month} + */ + public static Month monthEnum(Date date) { + return DateTime.of(date).monthEnum(); + } + + /** + * 获得指定日期是所在年份的第几周
+ * 此方法返回值与一周的第一天有关,比如:
+ * 2016年1月3日为周日,如果一周的第一天为周日,那这天是第二周(返回2)
+ * 如果一周的第一天为周一,那这天是第一周(返回1)
+ * 跨年的那个星期得到的结果总是1 + * + * @param date 日期 + * @return 周 + * @see DateTime#setFirstDayOfWeek(Week) + */ + public static int weekOfYear(Date date) { + return DateTime.of(date).weekOfYear(); + } + + /** + * 获得指定日期是所在月份的第几周
+ * + * @param date 日期 + * @return 周 + */ + public static int weekOfMonth(Date date) { + return DateTime.of(date).weekOfMonth(); + } + + /** + * 获得指定日期是这个日期所在月份的第几天
+ * + * @param date 日期 + * @return 天 + */ + public static int dayOfMonth(Date date) { + return DateTime.of(date).dayOfMonth(); + } + + /** + * 获得指定日期是这个日期所在年的第几天 + * + * @param date 日期 + * @return 天 + * @since 5.3.6 + */ + public static int dayOfYear(Date date) { + return DateTime.of(date).dayOfYear(); + } + + /** + * 获得指定日期是星期几,1表示周日,2表示周一 + * + * @param date 日期 + * @return 天 + */ + public static int dayOfWeek(Date date) { + return DateTime.of(date).dayOfWeek(); + } + + /** + * 获得指定日期是星期几 + * + * @param date 日期 + * @return {@link Week} + */ + public static Week dayOfWeekEnum(Date date) { + return DateTime.of(date).dayOfWeekEnum(); + } + + /** + * 是否为周末(周六或周日) + * + * @param date 判定的日期{@link Date} + * @return 是否为周末(周六或周日) + * @since 5.7.6 + */ + public static boolean isWeekend(Date date) { + final Week week = dayOfWeekEnum(date); + return Week.SATURDAY == week || Week.SUNDAY == week; + } + + /** + * 获得指定日期的小时数部分
+ * + * @param date 日期 + * @param is24HourClock 是否24小时制 + * @return 小时数 + */ + public static int hour(Date date, boolean is24HourClock) { + return DateTime.of(date).hour(is24HourClock); + } + + /** + * 获得指定日期的分钟数部分
+ * 例如:10:04:15.250 =》 4 + * + * @param date 日期 + * @return 分钟数 + */ + public static int minute(Date date) { + return DateTime.of(date).minute(); + } + + /** + * 获得指定日期的秒数部分
+ * + * @param date 日期 + * @return 秒数 + */ + public static int second(Date date) { + return DateTime.of(date).second(); + } + + /** + * 获得指定日期的毫秒数部分
+ * + * @param date 日期 + * @return 毫秒数 + */ + public static int millisecond(Date date) { + return DateTime.of(date).millisecond(); + } + + /** + * 是否为上午 + * + * @param date 日期 + * @return 是否为上午 + */ + public static boolean isAM(Date date) { + return DateTime.of(date).isAM(); + } + + /** + * 是否为下午 + * + * @param date 日期 + * @return 是否为下午 + */ + public static boolean isPM(Date date) { + return DateTime.of(date).isPM(); + } + + /** + * @return 今年 + */ + public static int thisYear() { + return year(date()); + } + + /** + * @return 当前月份 + */ + public static int thisMonth() { + return month(date()); + } + + /** + * @return 当前月份 {@link Month} + */ + public static Month thisMonthEnum() { + return monthEnum(date()); + } + + /** + * @return 当前日期所在年份的第几周 + */ + public static int thisWeekOfYear() { + return weekOfYear(date()); + } + + /** + * @return 当前日期所在月份的第几周 + */ + public static int thisWeekOfMonth() { + return weekOfMonth(date()); + } + + /** + * @return 当前日期是这个日期所在月份的第几天 + */ + public static int thisDayOfMonth() { + return dayOfMonth(date()); + } + + /** + * @return 当前日期是星期几 + */ + public static int thisDayOfWeek() { + return dayOfWeek(date()); + } + + /** + * @return 当前日期是星期几 {@link Week} + */ + public static Week thisDayOfWeekEnum() { + return dayOfWeekEnum(date()); + } + + /** + * @param is24HourClock 是否24小时制 + * @return 当前日期的小时数部分
+ */ + public static int thisHour(boolean is24HourClock) { + return hour(date(), is24HourClock); + } + + /** + * @return 当前日期的分钟数部分
+ */ + public static int thisMinute() { + return minute(date()); + } + + /** + * @return 当前日期的秒数部分
+ */ + public static int thisSecond() { + return second(date()); + } + + /** + * @return 当前日期的毫秒数部分
+ */ + public static int thisMillisecond() { + return millisecond(date()); + } + // -------------------------------------------------------------- Part of Date end + + /** + * 获得指定日期年份和季节
+ * 格式:[20131]表示2013年第一季度 + * + * @param date 日期 + * @return Quarter ,类似于 20132 + */ + public static String yearAndQuarter(Date date) { + return yearAndQuarter(calendar(date)); + } + + /** + * 获得指定日期区间内的年份和季节
+ * + * @param startDate 起始日期(包含) + * @param endDate 结束日期(包含) + * @return 季度列表 ,元素类似于 20132 + */ + public static LinkedHashSet yearAndQuarter(Date startDate, Date endDate) { + if (startDate == null || endDate == null) { + return new LinkedHashSet<>(0); + } + return yearAndQuarter(startDate.getTime(), endDate.getTime()); + } + // ------------------------------------ Format start ---------------------------------------------- + + /** + * 格式化日期时间
+ * 格式 yyyy-MM-dd HH:mm:ss + * + * @param localDateTime 被格式化的日期 + * @return 格式化后的字符串 + */ + public static String formatLocalDateTime(LocalDateTime localDateTime) { + return LocalDateTimeUtil.formatNormal(localDateTime); + } + + /** + * 根据特定格式格式化日期 + * + * @param localDateTime 被格式化的日期 + * @param format 日期格式,常用格式见: {@link DatePattern} + * @return 格式化后的字符串 + */ + public static String format(LocalDateTime localDateTime, String format) { + return LocalDateTimeUtil.format(localDateTime, format); + } + + /** + * 根据特定格式格式化日期 + * + * @param date 被格式化的日期 + * @param format 日期格式,常用格式见: {@link DatePattern} {@link DatePattern#NORM_DATETIME_PATTERN} + * @return 格式化后的字符串 + */ + public static String format(Date date, String format) { + if (null == date || StrUtil.isBlank(format)) { + return null; + } + + // 检查自定义格式 + if (GlobalCustomFormat.isCustomFormat(format)) { + return GlobalCustomFormat.format(date, format); + } + + TimeZone timeZone = null; + if (date instanceof DateTime) { + timeZone = ((DateTime) date).getTimeZone(); + } + return format(date, newSimpleFormat(format, null, timeZone)); + } + + /** + * 根据特定格式格式化日期 + * + * @param date 被格式化的日期 + * @param format {@link DatePrinter} 或 {@link FastDateFormat} {@link DatePattern#NORM_DATETIME_FORMAT} + * @return 格式化后的字符串 + */ + public static String format(Date date, DatePrinter format) { + if (null == format || null == date) { + return null; + } + return format.format(date); + } + + /** + * 根据特定格式格式化日期 + * + * @param date 被格式化的日期 + * @param format {@link SimpleDateFormat} + * @return 格式化后的字符串 + */ + public static String format(Date date, DateFormat format) { + if (null == format || null == date) { + return null; + } + return format.format(date); + } + + /** + * 根据特定格式格式化日期 + * + * @param date 被格式化的日期 + * @param format {@link SimpleDateFormat} {@link DatePattern#NORM_DATETIME_FORMATTER} + * @return 格式化后的字符串 + * @since 5.0.0 + */ + public static String format(Date date, DateTimeFormatter format) { + if (null == format || null == date) { + return null; + } + // java.time.temporal.UnsupportedTemporalTypeException: Unsupported field: YearOfEra + // 出现以上报错时,表示Instant时间戳没有时区信息,赋予默认时区 + return TemporalAccessorUtil.format(date.toInstant(), format); + } + + /** + * 格式化日期时间
+ * 格式 yyyy-MM-dd HH:mm:ss + * + * @param date 被格式化的日期 + * @return 格式化后的日期 + */ + public static String formatDateTime(Date date) { + if (null == date) { + return null; + } + return DatePattern.NORM_DATETIME_FORMAT.format(date); + } + + /** + * 格式化日期部分(不包括时间)
+ * 格式 yyyy-MM-dd + * + * @param date 被格式化的日期 + * @return 格式化后的字符串 + */ + public static String formatDate(Date date) { + if (null == date) { + return null; + } + return DatePattern.NORM_DATE_FORMAT.format(date); + } + + /** + * 格式化时间
+ * 格式 HH:mm:ss + * + * @param date 被格式化的日期 + * @return 格式化后的字符串 + * @since 3.0.1 + */ + public static String formatTime(Date date) { + if (null == date) { + return null; + } + return DatePattern.NORM_TIME_FORMAT.format(date); + } + + /** + * 格式化为Http的标准日期格式
+ * 标准日期格式遵循RFC 1123规范,格式类似于:Fri, 31 Dec 1999 23:59:59 GMT + * + * @param date 被格式化的日期 + * @return HTTP标准形式日期字符串 + */ + public static String formatHttpDate(Date date) { + if (null == date) { + return null; + } + return DatePattern.HTTP_DATETIME_FORMAT.format(date); + } + + /** + * 格式化为中文日期格式,如果isUppercase为false,则返回类似:2018年10月24日,否则返回二〇一八年十月二十四日 + * + * @param date 被格式化的日期 + * @param isUppercase 是否采用大写形式 + * @param withTime 是否包含时间部分 + * @return 中文日期字符串 + * @since 5.3.9 + */ + public static String formatChineseDate(Date date, boolean isUppercase, boolean withTime) { + if (null == date) { + return null; + } + + if (!isUppercase) { + return (withTime ? DatePattern.CHINESE_DATE_TIME_FORMAT : DatePattern.CHINESE_DATE_FORMAT).format(date); + } + + return CalendarUtil.formatChineseDate(CalendarUtil.calendar(date), withTime); + } + // ------------------------------------ Format end ---------------------------------------------- + + // ------------------------------------ Parse start ---------------------------------------------- + + /** + * 构建LocalDateTime对象
+ * 格式:yyyy-MM-dd HH:mm:ss + * + * @param dateStr 时间字符串(带格式) + * @return LocalDateTime对象 + */ + public static LocalDateTime parseLocalDateTime(CharSequence dateStr) { + return parseLocalDateTime(dateStr, DatePattern.NORM_DATETIME_PATTERN); + } + + /** + * 构建LocalDateTime对象 + * + * @param dateStr 时间字符串(带格式) + * @param format 使用{@link DatePattern}定义的格式 + * @return LocalDateTime对象 + */ + public static LocalDateTime parseLocalDateTime(CharSequence dateStr, String format) { + return LocalDateTimeUtil.parse(dateStr, format); + } + + /** + * 构建DateTime对象 + * + * @param dateStr Date字符串 + * @param dateFormat 格式化器 {@link SimpleDateFormat} + * @return DateTime对象 + */ + public static DateTime parse(CharSequence dateStr, DateFormat dateFormat) { + return new DateTime(dateStr, dateFormat); + } + + /** + * 构建DateTime对象 + * + * @param dateStr Date字符串 + * @param parser 格式化器,{@link FastDateFormat} + * @return DateTime对象 + */ + public static DateTime parse(CharSequence dateStr, DateParser parser) { + return new DateTime(dateStr, parser); + } + + /** + * 构建DateTime对象 + * + * @param dateStr Date字符串 + * @param parser 格式化器,{@link FastDateFormat} + * @param lenient 是否宽容模式 + * @return DateTime对象 + * @since 5.7.14 + */ + public static DateTime parse(CharSequence dateStr, DateParser parser, boolean lenient) { + return new DateTime(dateStr, parser, lenient); + } + + /** + * 构建DateTime对象 + * + * @param dateStr Date字符串 + * @param formatter 格式化器,{@link DateTimeFormatter} + * @return DateTime对象 + * @since 5.0.0 + */ + public static DateTime parse(CharSequence dateStr, DateTimeFormatter formatter) { + return new DateTime(dateStr, formatter); + } + + /** + * 将特定格式的日期转换为Date对象 + * + * @param dateStr 特定格式的日期 + * @param format 格式,例如yyyy-MM-dd + * @return 日期对象 + */ + public static DateTime parse(CharSequence dateStr, String format) { + return new DateTime(dateStr, format); + } + + /** + * 将特定格式的日期转换为Date对象 + * + * @param dateStr 特定格式的日期 + * @param format 格式,例如yyyy-MM-dd + * @param locale 区域信息 + * @return 日期对象 + * @since 4.5.18 + */ + public static DateTime parse(CharSequence dateStr, String format, Locale locale) { + if (GlobalCustomFormat.isCustomFormat(format)) { + // 自定义格式化器忽略Locale + return new DateTime(GlobalCustomFormat.parse(dateStr, format)); + } + return new DateTime(dateStr, DateUtil.newSimpleFormat(format, locale, null)); + } + + /** + * 通过给定的日期格式解析日期时间字符串。
+ * 传入的日期格式会逐个尝试,直到解析成功,返回{@link DateTime}对象,否则抛出{@link DateException}异常。 + * + * @param str 日期时间字符串,非空 + * @param parsePatterns 需要尝试的日期时间格式数组,非空, 见SimpleDateFormat + * @return 解析后的Date + * @throws IllegalArgumentException if the date string or pattern array is null + * @throws DateException if none of the date patterns were suitable + * @since 5.3.11 + */ + public static DateTime parse(String str, String... parsePatterns) throws DateException { + return new DateTime(CalendarUtil.parseByPatterns(str, parsePatterns)); + } + + /** + * 解析日期时间字符串,格式支持: + * + *
+	 * yyyy-MM-dd HH:mm:ss
+	 * yyyy/MM/dd HH:mm:ss
+	 * yyyy.MM.dd HH:mm:ss
+	 * yyyy年MM月dd日 HH:mm:ss
+	 * 
+ * + * @param dateString 标准形式的时间字符串 + * @return 日期对象 + */ + public static DateTime parseDateTime(CharSequence dateString) { + dateString = normalize(dateString); + return parse(dateString, DatePattern.NORM_DATETIME_FORMAT); + } + + /** + * 解析日期字符串,忽略时分秒,支持的格式包括: + *
+	 * yyyy-MM-dd
+	 * yyyy/MM/dd
+	 * yyyy.MM.dd
+	 * yyyy年MM月dd日
+	 * 
+ * + * @param dateString 标准形式的日期字符串 + * @return 日期对象 + */ + public static DateTime parseDate(CharSequence dateString) { + dateString = normalize(dateString); + return parse(dateString, DatePattern.NORM_DATE_FORMAT); + } + + /** + * 解析时间,格式HH:mm:ss,日期部分默认为1970-01-01 + * + * @param timeString 标准形式的日期字符串 + * @return 日期对象 + */ + public static DateTime parseTime(CharSequence timeString) { + timeString = normalize(timeString); + return parse(timeString, DatePattern.NORM_TIME_FORMAT); + } + + /** + * 解析时间,格式HH:mm 或 HH:mm:ss,日期默认为今天 + * + * @param timeString 标准形式的日期字符串 + * @return 日期对象 + * @since 3.1.1 + */ + public static DateTime parseTimeToday(CharSequence timeString) { + timeString = StrUtil.format("{} {}", today(), timeString); + if (1 == StrUtil.count(timeString, ':')) { + // 时间格式为 HH:mm + return parse(timeString, DatePattern.NORM_DATETIME_MINUTE_PATTERN); + } else { + // 时间格式为 HH:mm:ss + return parse(timeString, DatePattern.NORM_DATETIME_FORMAT); + } + } + + /** + * 解析UTC时间,格式:
+ *
    + *
  1. yyyy-MM-dd'T'HH:mm:ss'Z'
  2. + *
  3. yyyy-MM-dd'T'HH:mm:ss.SSS'Z'
  4. + *
  5. yyyy-MM-dd'T'HH:mm:ssZ
  6. + *
  7. yyyy-MM-dd'T'HH:mm:ss.SSSZ
  8. + *
  9. yyyy-MM-dd'T'HH:mm:ss+0800
  10. + *
  11. yyyy-MM-dd'T'HH:mm:ss+08:00
  12. + *
+ * + * @param utcString UTC时间 + * @return 日期对象 + * @since 4.1.14 + */ + public static DateTime parseUTC(String utcString) { + if (utcString == null) { + return null; + } + final int length = utcString.length(); + if (StrUtil.contains(utcString, 'Z')) { + if (length == DatePattern.UTC_PATTERN.length() - 4) { + // 格式类似:2018-09-13T05:34:31Z,-4表示减去4个单引号的长度 + return parse(utcString, DatePattern.UTC_FORMAT); + } + + final int patternLength = DatePattern.UTC_MS_PATTERN.length(); + // 格式类似:2018-09-13T05:34:31.999Z,-4表示减去4个单引号的长度 + // -4 ~ -6范围表示匹配毫秒1~3位的情况 + if (length <= patternLength - 4 && length >= patternLength - 6) { + return parse(utcString, DatePattern.UTC_MS_FORMAT); + } + } else if (StrUtil.contains(utcString, '+')) { + // 去除类似2019-06-01T19:45:43 +08:00加号前的空格 + utcString = utcString.replace(" +", "+"); + final String zoneOffset = StrUtil.subAfter(utcString, '+', true); + if (StrUtil.isBlank(zoneOffset)) { + throw new DateException("Invalid format: [{}]", utcString); + } + if (!StrUtil.contains(zoneOffset, ':')) { + // +0800转换为+08:00 + final String pre = StrUtil.subBefore(utcString, '+', true); + utcString = pre + "+" + zoneOffset.substring(0, 2) + ":" + "00"; + } + + if (StrUtil.contains(utcString, CharUtil.DOT)) { + // 带毫秒,格式类似:2018-09-13T05:34:31.999+08:00 + utcString = normalizeMillSeconds(utcString, ".", "+"); + return parse(utcString, DatePattern.UTC_MS_WITH_XXX_OFFSET_FORMAT); + } else { + // 格式类似:2018-09-13T05:34:31+08:00 + return parse(utcString, DatePattern.UTC_WITH_XXX_OFFSET_FORMAT); + } + } else if(ReUtil.contains("-\\d{2}:?00", utcString)){ + // Issue#2612,类似 2022-09-14T23:59:00-08:00 或者 2022-09-14T23:59:00-0800 + + // 去除类似2019-06-01T19:45:43 -08:00加号前的空格 + utcString = utcString.replace(" -", "-"); + if(':' != utcString.charAt(utcString.length() - 3)){ + utcString = utcString.substring(0, utcString.length() - 2) + ":00"; + } + + if (StrUtil.contains(utcString, CharUtil.DOT)) { + // 带毫秒,格式类似:2018-09-13T05:34:31.999-08:00 + utcString = normalizeMillSeconds(utcString, ".", "-"); + return new DateTime(utcString, DatePattern.UTC_MS_WITH_XXX_OFFSET_FORMAT); + } else { + // 格式类似:2018-09-13T05:34:31-08:00 + return new DateTime(utcString, DatePattern.UTC_WITH_XXX_OFFSET_FORMAT); + } + } else { + if (length == DatePattern.UTC_SIMPLE_PATTERN.length() - 2) { + // 格式类似:2018-09-13T05:34:31 + return parse(utcString, DatePattern.UTC_SIMPLE_FORMAT); + } else if (length == DatePattern.UTC_SIMPLE_PATTERN.length() - 5) { + // 格式类似:2018-09-13T05:34 + return parse(utcString + ":00", DatePattern.UTC_SIMPLE_FORMAT); + } else if (StrUtil.contains(utcString, CharUtil.DOT)) { + // 可能为: 2021-03-17T06:31:33.99 + utcString = normalizeMillSeconds(utcString, ".", null); + return parse(utcString, DatePattern.UTC_SIMPLE_MS_FORMAT); + } + } + // 没有更多匹配的时间格式 + throw new DateException("No format fit for date String [{}] !", utcString); + } + + /** + * 解析CST时间,格式:
+ *
    + *
  1. EEE MMM dd HH:mm:ss z yyyy(例如:Wed Aug 01 00:00:00 CST 2012)
  2. + *
+ * + * @param cstString UTC时间 + * @return 日期对象 + * @since 4.6.9 + */ + public static DateTime parseCST(CharSequence cstString) { + if (cstString == null) { + return null; + } + + return parse(cstString, DatePattern.JDK_DATETIME_FORMAT); + } + + /** + * 将日期字符串转换为{@link DateTime}对象,格式:
+ *
    + *
  1. yyyy-MM-dd HH:mm:ss
  2. + *
  3. yyyy/MM/dd HH:mm:ss
  4. + *
  5. yyyy.MM.dd HH:mm:ss
  6. + *
  7. yyyy年MM月dd日 HH时mm分ss秒
  8. + *
  9. yyyy-MM-dd
  10. + *
  11. yyyy/MM/dd
  12. + *
  13. yyyy.MM.dd
  14. + *
  15. HH:mm:ss
  16. + *
  17. HH时mm分ss秒
  18. + *
  19. yyyy-MM-dd HH:mm
  20. + *
  21. yyyy-MM-dd HH:mm:ss.SSS
  22. + *
  23. yyyy-MM-dd HH:mm:ss.SSSSSS
  24. + *
  25. yyyyMMddHHmmss
  26. + *
  27. yyyyMMddHHmmssSSS
  28. + *
  29. yyyyMMdd
  30. + *
  31. EEE, dd MMM yyyy HH:mm:ss z
  32. + *
  33. EEE MMM dd HH:mm:ss zzz yyyy
  34. + *
  35. yyyy-MM-dd'T'HH:mm:ss'Z'
  36. + *
  37. yyyy-MM-dd'T'HH:mm:ss.SSS'Z'
  38. + *
  39. yyyy-MM-dd'T'HH:mm:ssZ
  40. + *
  41. yyyy-MM-dd'T'HH:mm:ss.SSSZ
  42. + *
+ * + * @param dateCharSequence 日期字符串 + * @return 日期 + */ + public static DateTime parse(CharSequence dateCharSequence) { + if (StrUtil.isBlank(dateCharSequence)) { + return null; + } + String dateStr = dateCharSequence.toString(); + // 去掉两边空格并去掉中文日期中的“日”和“秒”,以规范长度 + dateStr = StrUtil.removeAll(dateStr.trim(), '日', '秒'); + int length = dateStr.length(); + + if (NumberUtil.isNumber(dateStr)) { + // 纯数字形式 + if (length == DatePattern.PURE_DATETIME_PATTERN.length()) { + return parse(dateStr, DatePattern.PURE_DATETIME_FORMAT); + } else if (length == DatePattern.PURE_DATETIME_MS_PATTERN.length()) { + return parse(dateStr, DatePattern.PURE_DATETIME_MS_FORMAT); + } else if (length == DatePattern.PURE_DATE_PATTERN.length()) { + return parse(dateStr, DatePattern.PURE_DATE_FORMAT); + } else if (length == DatePattern.PURE_TIME_PATTERN.length()) { + return parse(dateStr, DatePattern.PURE_TIME_FORMAT); + } + } else if (ReUtil.isMatch(PatternPool.TIME, dateStr)) { + // HH:mm:ss 或者 HH:mm 时间格式匹配单独解析 + return parseTimeToday(dateStr); + } else if (StrUtil.containsAnyIgnoreCase(dateStr, wtb)) { + // JDK的Date对象toString默认格式,类似于: + // Tue Jun 4 16:25:15 +0800 2019 + // Thu May 16 17:57:18 GMT+08:00 2019 + // Wed Aug 01 00:00:00 CST 2012 + return parseCST(dateStr); + } else if (StrUtil.contains(dateStr, 'T')) { + // UTC时间 + return parseUTC(dateStr); + } + + //标准日期格式(包括单个数字的日期时间) + dateStr = normalize(dateStr); + if (ReUtil.isMatch(DatePattern.REGEX_NORM, dateStr)) { + final int colonCount = StrUtil.count(dateStr, CharUtil.COLON); + switch (colonCount) { + case 0: + // yyyy-MM-dd + return parse(dateStr, DatePattern.NORM_DATE_FORMAT); + case 1: + // yyyy-MM-dd HH:mm + return parse(dateStr, DatePattern.NORM_DATETIME_MINUTE_FORMAT); + case 2: + final int indexOfDot = StrUtil.indexOf(dateStr, CharUtil.DOT); + if (indexOfDot > 0) { + final int length1 = dateStr.length(); + // yyyy-MM-dd HH:mm:ss.SSS 或者 yyyy-MM-dd HH:mm:ss.SSSSSS + if (length1 - indexOfDot > 4) { + // 类似yyyy-MM-dd HH:mm:ss.SSSSSS,采取截断操作 + dateStr = StrUtil.subPre(dateStr, indexOfDot + 4); + } + return parse(dateStr, DatePattern.NORM_DATETIME_MS_FORMAT); + } + // yyyy-MM-dd HH:mm:ss + return parse(dateStr, DatePattern.NORM_DATETIME_FORMAT); + } + } + + // 没有更多匹配的时间格式 + throw new DateException("No format fit for date String [{}] !", dateStr); + } + + // ------------------------------------ Parse end ---------------------------------------------- + + // ------------------------------------ Offset start ---------------------------------------------- + + /** + * 修改日期为某个时间字段起始时间 + * + * @param date {@link Date} + * @param dateField 保留到的时间字段,如定义为 {@link DateField#SECOND},表示这个字段不变,这个字段以下字段全部归0 + * @return {@link DateTime} + * @since 4.5.7 + */ + public static DateTime truncate(Date date, DateField dateField) { + return new DateTime(truncate(calendar(date), dateField)); + } + + /** + * 修改日期为某个时间字段四舍五入时间 + * + * @param date {@link Date} + * @param dateField 时间字段 + * @return {@link DateTime} + * @since 4.5.7 + */ + public static DateTime round(Date date, DateField dateField) { + return new DateTime(round(calendar(date), dateField)); + } + + /** + * 修改日期为某个时间字段结束时间 + * + * @param date {@link Date} + * @param dateField 保留到的时间字段,如定义为 {@link DateField#SECOND},表示这个字段不变,这个字段以下字段全部取最大值 + * @return {@link DateTime} + * @since 4.5.7 + */ + public static DateTime ceiling(Date date, DateField dateField) { + return new DateTime(ceiling(calendar(date), dateField)); + } + + /** + * 修改日期为某个时间字段结束时间
+ * 可选是否归零毫秒。 + * + *

+ * 有时候由于毫秒部分必须为0(如MySQL数据库中),因此在此加上选项。 + *

+ * + * @param date {@link Date} + * @param dateField 时间字段 + * @param truncateMillisecond 是否毫秒归零 + * @return {@link DateTime} + * @since 4.5.7 + */ + public static DateTime ceiling(Date date, DateField dateField, boolean truncateMillisecond) { + return new DateTime(ceiling(calendar(date), dateField, truncateMillisecond)); + } + + /** + * 获取秒级别的开始时间,即毫秒部分设置为0 + * + * @param date 日期 + * @return {@link DateTime} + * @since 4.6.2 + */ + public static DateTime beginOfSecond(Date date) { + return new DateTime(beginOfSecond(calendar(date))); + } + + /** + * 获取秒级别的结束时间,即毫秒设置为999 + * + * @param date 日期 + * @return {@link DateTime} + * @since 4.6.2 + */ + public static DateTime endOfSecond(Date date) { + return new DateTime(endOfSecond(calendar(date))); + } + + /** + * 获取某小时的开始时间 + * + * @param date 日期 + * @return {@link DateTime} + */ + public static DateTime beginOfHour(Date date) { + return new DateTime(beginOfHour(calendar(date))); + } + + /** + * 获取某小时的结束时间 + * + * @param date 日期 + * @return {@link DateTime} + */ + public static DateTime endOfHour(Date date) { + return new DateTime(endOfHour(calendar(date))); + } + + /** + * 获取某分钟的开始时间 + * + * @param date 日期 + * @return {@link DateTime} + */ + public static DateTime beginOfMinute(Date date) { + return new DateTime(beginOfMinute(calendar(date))); + } + + /** + * 获取某分钟的结束时间 + * + * @param date 日期 + * @return {@link DateTime} + */ + public static DateTime endOfMinute(Date date) { + return new DateTime(endOfMinute(calendar(date))); + } + + /** + * 获取某天的开始时间 + * + * @param date 日期 + * @return {@link DateTime} + */ + public static DateTime beginOfDay(Date date) { + return new DateTime(beginOfDay(calendar(date))); + } + + /** + * 获取某天的结束时间 + * + * @param date 日期 + * @return {@link DateTime} + */ + public static DateTime endOfDay(Date date) { + return new DateTime(endOfDay(calendar(date))); + } + + /** + * 获取某周的开始时间,周一定为一周的开始时间 + * + * @param date 日期 + * @return {@link DateTime} + */ + public static DateTime beginOfWeek(Date date) { + return new DateTime(beginOfWeek(calendar(date))); + } + + /** + * 获取某周的开始时间 + * + * @param date 日期 + * @param isMondayAsFirstDay 是否周一做为一周的第一天(false表示周日做为第一天) + * @return {@link DateTime} + * @since 5.4.0 + */ + public static DateTime beginOfWeek(Date date, boolean isMondayAsFirstDay) { + return new DateTime(beginOfWeek(calendar(date), isMondayAsFirstDay)); + } + + /** + * 获取某周的结束时间,周日定为一周的结束 + * + * @param date 日期 + * @return {@link DateTime} + */ + public static DateTime endOfWeek(Date date) { + return new DateTime(endOfWeek(calendar(date))); + } + + /** + * 获取某周的结束时间 + * + * @param date 日期 + * @param isSundayAsLastDay 是否周日做为一周的最后一天(false表示周六做为最后一天) + * @return {@link DateTime} + * @since 5.4.0 + */ + public static DateTime endOfWeek(Date date, boolean isSundayAsLastDay) { + return new DateTime(endOfWeek(calendar(date), isSundayAsLastDay)); + } + + /** + * 获取某月的开始时间 + * + * @param date 日期 + * @return {@link DateTime} + */ + public static DateTime beginOfMonth(Date date) { + return new DateTime(beginOfMonth(calendar(date))); + } + + /** + * 获取某月的结束时间 + * + * @param date 日期 + * @return {@link DateTime} + */ + public static DateTime endOfMonth(Date date) { + return new DateTime(endOfMonth(calendar(date))); + } + + /** + * 获取某季度的开始时间 + * + * @param date 日期 + * @return {@link DateTime} + */ + public static DateTime beginOfQuarter(Date date) { + return new DateTime(beginOfQuarter(calendar(date))); + } + + /** + * 获取某季度的结束时间 + * + * @param date 日期 + * @return {@link DateTime} + */ + public static DateTime endOfQuarter(Date date) { + return new DateTime(endOfQuarter(calendar(date))); + } + + /** + * 获取某年的开始时间 + * + * @param date 日期 + * @return {@link DateTime} + */ + public static DateTime beginOfYear(Date date) { + return new DateTime(beginOfYear(calendar(date))); + } + + /** + * 获取某年的结束时间 + * + * @param date 日期 + * @return {@link DateTime} + */ + public static DateTime endOfYear(Date date) { + return new DateTime(endOfYear(calendar(date))); + } + // --------------------------------------------------- Offset for now + + /** + * 昨天 + * + * @return 昨天 + */ + public static DateTime yesterday() { + return offsetDay(new DateTime(), -1); + } + + /** + * 明天 + * + * @return 明天 + * @since 3.0.1 + */ + public static DateTime tomorrow() { + return offsetDay(new DateTime(), 1); + } + + /** + * 上周 + * + * @return 上周 + */ + public static DateTime lastWeek() { + return offsetWeek(new DateTime(), -1); + } + + /** + * 下周 + * + * @return 下周 + * @since 3.0.1 + */ + public static DateTime nextWeek() { + return offsetWeek(new DateTime(), 1); + } + + /** + * 上个月 + * + * @return 上个月 + */ + public static DateTime lastMonth() { + return offsetMonth(new DateTime(), -1); + } + + /** + * 下个月 + * + * @return 下个月 + * @since 3.0.1 + */ + public static DateTime nextMonth() { + return offsetMonth(new DateTime(), 1); + } + + /** + * 偏移毫秒数 + * + * @param date 日期 + * @param offset 偏移毫秒数,正数向未来偏移,负数向历史偏移 + * @return 偏移后的日期 + */ + public static DateTime offsetMillisecond(Date date, int offset) { + return offset(date, DateField.MILLISECOND, offset); + } + + /** + * 偏移秒数 + * + * @param date 日期 + * @param offset 偏移秒数,正数向未来偏移,负数向历史偏移 + * @return 偏移后的日期 + */ + public static DateTime offsetSecond(Date date, int offset) { + return offset(date, DateField.SECOND, offset); + } + + /** + * 偏移分钟 + * + * @param date 日期 + * @param offset 偏移分钟数,正数向未来偏移,负数向历史偏移 + * @return 偏移后的日期 + */ + public static DateTime offsetMinute(Date date, int offset) { + return offset(date, DateField.MINUTE, offset); + } + + /** + * 偏移小时 + * + * @param date 日期 + * @param offset 偏移小时数,正数向未来偏移,负数向历史偏移 + * @return 偏移后的日期 + */ + public static DateTime offsetHour(Date date, int offset) { + return offset(date, DateField.HOUR_OF_DAY, offset); + } + + /**w + * 偏移天 + * + * @param date 日期 + * @param offset 偏移天数,正数向未来偏移,负数向历史偏移 + * @return 偏移后的日期 + */ + public static DateTime offsetDay(Date date, int offset) { + return offset(date, DateField.DAY_OF_YEAR, offset); + } + + /** + * 偏移周 + * + * @param date 日期 + * @param offset 偏移周数,正数向未来偏移,负数向历史偏移 + * @return 偏移后的日期 + */ + public static DateTime offsetWeek(Date date, int offset) { + return offset(date, DateField.WEEK_OF_YEAR, offset); + } + + /** + * 偏移月 + * + * @param date 日期 + * @param offset 偏移月数,正数向未来偏移,负数向历史偏移 + * @return 偏移后的日期 + */ + public static DateTime offsetMonth(Date date, int offset) { + return offset(date, DateField.MONTH, offset); + } + + /** + * 获取指定日期偏移指定时间后的时间,生成的偏移日期不影响原日期 + * + * @param date 基准日期 + * @param dateField 偏移的粒度大小(小时、天、月等){@link DateField} + * @param offset 偏移量,正数为向后偏移,负数为向前偏移 + * @return 偏移后的日期 + */ + public static DateTime offset(Date date, DateField dateField, int offset) { + return dateNew(date).offset(dateField, offset); + } + + // ------------------------------------ Offset end ---------------------------------------------- + + /** + * 判断两个日期相差的时长,只保留绝对值 + * + * @param beginDate 起始日期 + * @param endDate 结束日期 + * @param unit 相差的单位:相差 天{@link DateUnit#DAY}、小时{@link DateUnit#HOUR} 等 + * @return 日期差 + */ + public static long between(Date beginDate, Date endDate, DateUnit unit) { + return between(beginDate, endDate, unit, true); + } + + /** + * 判断两个日期相差的时长 + * + * @param beginDate 起始日期 + * @param endDate 结束日期 + * @param unit 相差的单位:相差 天{@link DateUnit#DAY}、小时{@link DateUnit#HOUR} 等 + * @param isAbs 日期间隔是否只保留绝对值正数 + * @return 日期差 + * @since 3.3.1 + */ + public static long between(Date beginDate, Date endDate, DateUnit unit, boolean isAbs) { + return new DateBetween(beginDate, endDate, isAbs).between(unit); + } + + /** + * 判断两个日期相差的毫秒数 + * + * @param beginDate 起始日期 + * @param endDate 结束日期 + * @return 日期差 + * @since 3.0.1 + */ + public static long betweenMs(Date beginDate, Date endDate) { + return new DateBetween(beginDate, endDate).between(DateUnit.MS); + } + + /** + * 判断两个日期相差的天数
+ * + *
+	 * 有时候我们计算相差天数的时候需要忽略时分秒。
+	 * 比如:2016-02-01 23:59:59和2016-02-02 00:00:00相差一秒
+	 * 如果isReset为{@code false}相差天数为0。
+	 * 如果isReset为{@code true}相差天数将被计算为1
+	 * 
+ * + * @param beginDate 起始日期 + * @param endDate 结束日期 + * @param isReset 是否重置时间为起始时间 + * @return 日期差 + * @since 3.0.1 + */ + public static long betweenDay(Date beginDate, Date endDate, boolean isReset) { + if (isReset) { + beginDate = beginOfDay(beginDate); + endDate = beginOfDay(endDate); + } + return between(beginDate, endDate, DateUnit.DAY); + } + + /** + * 计算指定时间区间内的周数 + * + * @param beginDate 开始时间 + * @param endDate 结束时间 + * @param isReset 是否重置时间为起始时间 + * @return 周数 + */ + public static long betweenWeek(Date beginDate, Date endDate, boolean isReset) { + if (isReset) { + beginDate = beginOfDay(beginDate); + endDate = beginOfDay(endDate); + } + return between(beginDate, endDate, DateUnit.WEEK); + } + + /** + * 计算两个日期相差月数
+ * 在非重置情况下,如果起始日期的天大于结束日期的天,月数要少算1(不足1个月) + * + * @param beginDate 起始日期 + * @param endDate 结束日期 + * @param isReset 是否重置时间为起始时间(重置天时分秒) + * @return 相差月数 + * @since 3.0.8 + */ + public static long betweenMonth(Date beginDate, Date endDate, boolean isReset) { + return new DateBetween(beginDate, endDate).betweenMonth(isReset); + } + + /** + * 计算两个日期相差年数
+ * 在非重置情况下,如果起始日期的月大于结束日期的月,年数要少算1(不足1年) + * + * @param beginDate 起始日期 + * @param endDate 结束日期 + * @param isReset 是否重置时间为起始时间(重置月天时分秒) + * @return 相差年数 + * @since 3.0.8 + */ + public static long betweenYear(Date beginDate, Date endDate, boolean isReset) { + return new DateBetween(beginDate, endDate).betweenYear(isReset); + } + + /** + * 格式化日期间隔输出 + * + * @param beginDate 起始日期 + * @param endDate 结束日期 + * @param level 级别,按照天、小时、分、秒、毫秒分为5个等级 + * @return XX天XX小时XX分XX秒 + */ + public static String formatBetween(Date beginDate, Date endDate, BetweenFormatter.Level level) { + return formatBetween(between(beginDate, endDate, DateUnit.MS), level); + } + + /** + * 格式化日期间隔输出,精确到毫秒 + * + * @param beginDate 起始日期 + * @param endDate 结束日期 + * @return XX天XX小时XX分XX秒 + * @since 3.0.1 + */ + public static String formatBetween(Date beginDate, Date endDate) { + return formatBetween(between(beginDate, endDate, DateUnit.MS)); + } + + /** + * 格式化日期间隔输出 + * + * @param betweenMs 日期间隔 + * @param level 级别,按照天、小时、分、秒、毫秒分为5个等级 + * @return XX天XX小时XX分XX秒XX毫秒 + */ + public static String formatBetween(long betweenMs, BetweenFormatter.Level level) { + return new BetweenFormatter(betweenMs, level).format(); + } + + /** + * 格式化日期间隔输出,精确到毫秒 + * + * @param betweenMs 日期间隔 + * @return XX天XX小时XX分XX秒XX毫秒 + * @since 3.0.1 + */ + public static String formatBetween(long betweenMs) { + return new BetweenFormatter(betweenMs, BetweenFormatter.Level.MILLISECOND).format(); + } + + /** + * 当前日期是否在日期指定范围内
+ * 起始日期和结束日期可以互换 + * + * @param date 被检查的日期 + * @param beginDate 起始日期(包含) + * @param endDate 结束日期(包含) + * @return 是否在范围内 + * @since 3.0.8 + */ + public static boolean isIn(Date date, Date beginDate, Date endDate) { + if (date instanceof DateTime) { + return ((DateTime) date).isIn(beginDate, endDate); + } else { + return new DateTime(date).isIn(beginDate, endDate); + } + } + + /** + * 是否为相同时间
+ * 此方法比较两个日期的时间戳是否相同 + * + * @param date1 日期1 + * @param date2 日期2 + * @return 是否为相同时间 + * @since 4.1.13 + */ + public static boolean isSameTime(Date date1, Date date2) { + return date1.compareTo(date2) == 0; + } + + /** + * 比较两个日期是否为同一天 + * + * @param date1 日期1 + * @param date2 日期2 + * @return 是否为同一天 + * @since 4.1.13 + */ + public static boolean isSameDay(final Date date1, final Date date2) { + if (date1 == null || date2 == null) { + throw new IllegalArgumentException("The date must not be null"); + } + return CalendarUtil.isSameDay(calendar(date1), calendar(date2)); + } + + /** + * 比较两个日期是否为同一周 + * + * @param date1 日期1 + * @param date2 日期2 + * @param isMon 是否为周一。国内第一天为星期一,国外第一天为星期日 + * @return 是否为同一周 + */ + public static boolean isSameWeek(final Date date1, final Date date2, boolean isMon) { + if (date1 == null || date2 == null) { + throw new IllegalArgumentException("The date must not be null"); + } + return CalendarUtil.isSameWeek(calendar(date1), calendar(date2), isMon); + } + + /** + * 比较两个日期是否为同一月 + * + * @param date1 日期1 + * @param date2 日期2 + * @return 是否为同一月 + * @since 5.4.1 + */ + public static boolean isSameMonth(final Date date1, final Date date2) { + if (date1 == null || date2 == null) { + throw new IllegalArgumentException("The date must not be null"); + } + return CalendarUtil.isSameMonth(calendar(date1), calendar(date2)); + } + + + /** + * 计时,常用于记录某段代码的执行时间,单位:纳秒 + * + * @param preTime 之前记录的时间 + * @return 时间差,纳秒 + */ + public static long spendNt(long preTime) { + return System.nanoTime() - preTime; + } + + /** + * 计时,常用于记录某段代码的执行时间,单位:毫秒 + * + * @param preTime 之前记录的时间 + * @return 时间差,毫秒 + */ + public static long spendMs(long preTime) { + return System.currentTimeMillis() - preTime; + } + + /** + * 格式化成yyMMddHHmm后转换为int型 + * + * @param date 日期 + * @return int + * @deprecated 2022年后结果溢出,此方法废弃 + */ + @Deprecated + public static int toIntSecond(Date date) { + return Integer.parseInt(DateUtil.format(date, "yyMMddHHmm")); + } + + /** + * 计时器
+ * 计算某个过程花费的时间,精确到毫秒 + * + * @return Timer + */ + public static TimeInterval timer() { + return new TimeInterval(); + + } + + /** + * 计时器
+ * 计算某个过程花费的时间,精确到毫秒 + * + * @param isNano 是否使用纳秒计数,false则使用毫秒 + * @return Timer + * @since 5.2.3 + */ + public static TimeInterval timer(boolean isNano) { + return new TimeInterval(isNano); + } + + /** + * 创建秒表{@link StopWatch},用于对代码块的执行时间计数 + *

+ * 使用方法如下: + * + *

+	 * StopWatch stopWatch = DateUtil.createStopWatch();
+	 *
+	 * // 任务1
+	 * stopWatch.start("任务一");
+	 * Thread.sleep(1000);
+	 * stopWatch.stop();
+	 *
+	 * // 任务2
+	 * stopWatch.start("任务二");
+	 * Thread.sleep(2000);
+	 * stopWatch.stop();
+	 *
+	 * // 打印出耗时
+	 * Console.log(stopWatch.prettyPrint());
+	 *
+	 * 
+ * + * @return {@link StopWatch} + * @since 5.2.3 + */ + public static StopWatch createStopWatch() { + return new StopWatch(); + } + + /** + * 创建秒表{@link StopWatch},用于对代码块的执行时间计数 + *

+ * 使用方法如下: + * + *

+	 * StopWatch stopWatch = DateUtil.createStopWatch("任务名称");
+	 *
+	 * // 任务1
+	 * stopWatch.start("任务一");
+	 * Thread.sleep(1000);
+	 * stopWatch.stop();
+	 *
+	 * // 任务2
+	 * stopWatch.start("任务二");
+	 * Thread.sleep(2000);
+	 * stopWatch.stop();
+	 *
+	 * // 打印出耗时
+	 * Console.log(stopWatch.prettyPrint());
+	 *
+	 * 
+ * + * @param id 用于标识秒表的唯一ID + * @return {@link StopWatch} + * @since 5.2.3 + */ + public static StopWatch createStopWatch(String id) { + return new StopWatch(id); + } + + /** + * 生日转为年龄,计算法定年龄 + * + * @param birthDay 生日,标准日期字符串 + * @return 年龄 + */ + public static int ageOfNow(String birthDay) { + return ageOfNow(parse(birthDay)); + } + + /** + * 生日转为年龄,计算法定年龄 + * + * @param birthDay 生日 + * @return 年龄 + */ + public static int ageOfNow(Date birthDay) { + return age(birthDay, date()); + } + + /** + * 是否闰年 + * + * @param year 年 + * @return 是否闰年 + */ + public static boolean isLeapYear(int year) { + return Year.isLeap(year); + } + + /** + * 计算相对于dateToCompare的年龄,常用于计算指定生日在某年的年龄 + * + * @param birthday 生日 + * @param dateToCompare 需要对比的日期 + * @return 年龄 + */ + public static int age(Date birthday, Date dateToCompare) { + Assert.notNull(birthday, "Birthday can not be null !"); + if (null == dateToCompare) { + dateToCompare = date(); + } + return age(birthday.getTime(), dateToCompare.getTime()); + } + + /** + * 判定给定开始时间经过某段时间后是否过期 + * + * @param startDate 开始时间 + * @param dateField 时间单位 + * @param timeLength 实际经过时长 + * @param endDate 被比较的时间,即有效期的截止时间。如果经过时长后的时间晚于截止时间,就表示过期 + * @return 是否过期 + * @since 3.1.1 + * @deprecated 此方法存在一定的歧义,容易产生误导,废弃。 + */ + @Deprecated + public static boolean isExpired(Date startDate, DateField dateField, int timeLength, Date endDate) { + final Date offsetDate = offset(startDate, dateField, timeLength); + return offsetDate.after(endDate); + } + + /** + * 判定在指定检查时间是否过期。 + * + *

+ * 以商品为例,startDate即生产日期,endDate即保质期的截止日期,checkDate表示在何时检查是否过期(一般为当前时间)
+ * endDate和startDate的差值即为保质期(按照毫秒计),checkDate和startDate的差值即为实际经过的时长,实际时长大于保质期表示超时。 + *

+ * + * @param startDate 开始时间 + * @param endDate 被比较的时间,即有效期的截止时间。如果经过时长后的时间晚于被检查的时间,就表示过期 + * @param checkDate 检查时间,可以是当前时间,既 + * @return 是否过期 + * @since 5.1.1 + * @deprecated 使用isIn方法 + */ + @Deprecated + public static boolean isExpired(Date startDate, Date endDate, Date checkDate) { + return betweenMs(startDate, checkDate) > betweenMs(startDate, endDate); + } + + /** + * HH:mm:ss 时间格式字符串转为秒数
+ * 参考:https://github.com/iceroot + * + * @param timeStr 字符串时分秒(HH:mm:ss)格式 + * @return 时分秒转换后的秒数 + * @since 3.1.2 + */ + public static int timeToSecond(String timeStr) { + if (StrUtil.isEmpty(timeStr)) { + return 0; + } + + final List hms = StrUtil.splitTrim(timeStr, StrUtil.C_COLON, 3); + int lastIndex = hms.size() - 1; + + int result = 0; + for (int i = lastIndex; i >= 0; i--) { + result += Integer.parseInt(hms.get(i)) * Math.pow(60, (lastIndex - i)); + } + return result; + } + + /** + * 秒数转为时间格式(HH:mm:ss)
+ * 参考:https://github.com/iceroot + * + * @param seconds 需要转换的秒数 + * @return 转换后的字符串 + * @since 3.1.2 + */ + public static String secondToTime(int seconds) { + if (seconds < 0) { + throw new IllegalArgumentException("Seconds must be a positive number!"); + } + + int hour = seconds / 3600; + int other = seconds % 3600; + int minute = other / 60; + int second = other % 60; + final StringBuilder sb = new StringBuilder(); + if (hour < 10) { + sb.append("0"); + } + sb.append(hour); + sb.append(":"); + if (minute < 10) { + sb.append("0"); + } + sb.append(minute); + sb.append(":"); + if (second < 10) { + sb.append("0"); + } + sb.append(second); + return sb.toString(); + } + + /** + * 创建日期范围生成器 + * + * @param start 起始日期时间(包括) + * @param end 结束日期时间 + * @param unit 步进单位 + * @return {@link DateRange} + */ + public static DateRange range(Date start, Date end, final DateField unit) { + return new DateRange(start, end, unit); + } + + /** + * 俩个时间区间取交集 + * + * @param start 开始区间 + * @param end 结束区间 + * @return true 包含 + * @author handy + * @since 5.7.21 + */ + public static List rangeContains(DateRange start, DateRange end) { + List startDateTimes = CollUtil.newArrayList((Iterable) start); + List endDateTimes = CollUtil.newArrayList((Iterable) end); + return startDateTimes.stream().filter(endDateTimes::contains).collect(Collectors.toList()); + } + + /** + * 俩个时间区间取差集(end - start) + * + * @param start 开始区间 + * @param end 结束区间 + * @return true 包含 + * @author handy + * @since 5.7.21 + */ + public static List rangeNotContains(DateRange start, DateRange end) { + List startDateTimes = CollUtil.newArrayList((Iterable) start); + List endDateTimes = CollUtil.newArrayList((Iterable) end); + return endDateTimes.stream().filter(item -> !startDateTimes.contains(item)).collect(Collectors.toList()); + } + + /** + * 按日期范围遍历,执行 function + * + * @param start 起始日期时间(包括) + * @param end 结束日期时间 + * @param unit 步进单位 + * @param func 每次遍历要执行的 function + * @param Date经过函数处理结果类型 + * @return 结果列表 + * @since 5.7.21 + */ + public static List rangeFunc(Date start, Date end, final DateField unit, Function func) { + if (start == null || end == null || start.after(end)) { + return Collections.emptyList(); + } + ArrayList list = new ArrayList<>(); + for (DateTime date : range(start, end, unit)) { + list.add(func.apply(date)); + } + return list; + } + + /** + * 按日期范围遍历,执行 consumer + * + * @param start 起始日期时间(包括) + * @param end 结束日期时间 + * @param unit 步进单位 + * @param consumer 每次遍历要执行的 consumer + * @since 5.7.21 + */ + public static void rangeConsume(Date start, Date end, final DateField unit, Consumer consumer) { + if (start == null || end == null || start.after(end)) { + return; + } + range(start, end, unit).forEach(consumer); + } + + /** + * 根据步进单位获取起始日期时间和结束日期时间的时间区间集合 + * + * @param start 起始日期时间 + * @param end 结束日期时间 + * @param unit 步进单位 + * @return {@link DateRange} + */ + public static List rangeToList(Date start, Date end, DateField unit) { + return CollUtil.newArrayList((Iterable) range(start, end, unit)); + } + + /** + * 根据步进单位和步进获取起始日期时间和结束日期时间的时间区间集合 + * + * @param start 起始日期时间 + * @param end 结束日期时间 + * @param unit 步进单位 + * @param step 步进 + * @return {@link DateRange} + * @since 5.7.16 + */ + public static List rangeToList(Date start, Date end, final DateField unit, int step) { + return CollUtil.newArrayList((Iterable) new DateRange(start, end, unit, step)); + } + + /** + * 通过生日计算星座 + * + * @param month 月,从0开始计数 + * @param day 天 + * @return 星座名 + * @since 4.4.3 + */ + public static String getZodiac(int month, int day) { + return Zodiac.getZodiac(month, day); + } + + /** + * 计算生肖,只计算1900年后出生的人 + * + * @param year 农历年 + * @return 生肖名 + * @since 4.4.3 + */ + public static String getChineseZodiac(int year) { + return Zodiac.getChineseZodiac(year); + } + + /** + * {@code null}安全的日期比较,{@code null}对象排在末尾 + * + * @param date1 日期1 + * @param date2 日期2 + * @return 比较结果,如果date1 < date2,返回数小于0,date1==date2返回0,date1 > date2 大于0 + * @since 4.6.2 + */ + public static int compare(Date date1, Date date2) { + return CompareUtil.compare(date1, date2); + } + + /** + * {@code null}安全的日期比较,并只比较指定格式; {@code null}对象排在末尾, 并指定日期格式; + * + * @param date1 日期1 + * @param date2 日期2 + * @param format 日期格式,常用格式见: {@link DatePattern}; 允许为空; date1 date2; eg: yyyy-MM-dd + * @return 比较结果,如果date1 < date2,返回数小于0,date1==date2返回0,date1 > date2 大于0 + * @author dazer + * @since 5.6.4 + */ + public static int compare(Date date1, Date date2, String format) { + if (format != null) { + if (date1 != null) { + date1 = parse(format(date1, format), format); + } + if (date2 != null) { + date2 = parse(format(date2, format), format); + } + } + return CompareUtil.compare(date1, date2); + } + + /** + * 纳秒转毫秒 + * + * @param duration 时长 + * @return 时长毫秒 + * @since 4.6.6 + */ + public static long nanosToMillis(long duration) { + return TimeUnit.NANOSECONDS.toMillis(duration); + } + + /** + * 纳秒转秒,保留小数 + * + * @param duration 时长 + * @return 秒 + * @since 4.6.6 + */ + public static double nanosToSeconds(long duration) { + return duration / 1_000_000_000.0; + } + + /** + * Date对象转换为{@link Instant}对象 + * + * @param date Date对象 + * @return {@link Instant}对象 + * @since 5.0.2 + */ + public static Instant toInstant(Date date) { + return null == date ? null : date.toInstant(); + } + + /** + * Date对象转换为{@link Instant}对象 + * + * @param temporalAccessor Date对象 + * @return {@link Instant}对象 + * @since 5.0.2 + */ + public static Instant toInstant(TemporalAccessor temporalAccessor) { + return TemporalAccessorUtil.toInstant(temporalAccessor); + } + + /** + * {@link Instant} 转换为 {@link LocalDateTime},使用系统默认时区 + * + * @param instant {@link Instant} + * @return {@link LocalDateTime} + * @see LocalDateTimeUtil#of(Instant) + * @since 5.0.5 + */ + public static LocalDateTime toLocalDateTime(Instant instant) { + return LocalDateTimeUtil.of(instant); + } + + /** + * {@link Date} 转换为 {@link LocalDateTime},使用系统默认时区 + * + * @param date {@link Date} + * @return {@link LocalDateTime} + * @see LocalDateTimeUtil#of(Date) + * @since 5.0.5 + */ + public static LocalDateTime toLocalDateTime(Date date) { + return LocalDateTimeUtil.of(date); + } + + /** + * {@link Date} 转换时区 + * + * @param date {@link Date} + * @param zoneId {@link ZoneId} + * @return {@link DateTime} + * @since 5.8.3 + */ + public static DateTime convertTimeZone(Date date, ZoneId zoneId) { + return new DateTime(date, ZoneUtil.toTimeZone(zoneId)); + } + + /** + * {@link Date} 转换时区 + * + * @param date {@link Date} + * @param timeZone {@link TimeZone} + * @return {@link DateTime} + * @since 5.8.3 + */ + public static DateTime convertTimeZone(Date date, TimeZone timeZone) { + return new DateTime(date, timeZone); + } + + /** + * 获得指定年份的总天数 + * + * @param year 年份 + * @return 天 + * @since 5.3.6 + */ + public static int lengthOfYear(int year) { + return Year.of(year).length(); + } + + /** + * 获得指定月份的总天数 + * + * @param month 月份 + * @param isLeapYear 是否闰年 + * @return 天 + * @since 5.4.2 + */ + public static int lengthOfMonth(int month, boolean isLeapYear) { + return java.time.Month.of(month).length(isLeapYear); + } + + /** + * 创建{@link SimpleDateFormat},注意此对象非线程安全!
+ * 此对象默认为严格格式模式,即parse时如果格式不正确会报错。 + * + * @param pattern 表达式 + * @return {@link SimpleDateFormat} + * @since 5.5.5 + */ + public static SimpleDateFormat newSimpleFormat(String pattern) { + return newSimpleFormat(pattern, null, null); + } + + /** + * 创建{@link SimpleDateFormat},注意此对象非线程安全!
+ * 此对象默认为严格格式模式,即parse时如果格式不正确会报错。 + * + * @param pattern 表达式 + * @param locale {@link Locale},{@code null}表示默认 + * @param timeZone {@link TimeZone},{@code null}表示默认 + * @return {@link SimpleDateFormat} + * @since 5.5.5 + */ + public static SimpleDateFormat newSimpleFormat(String pattern, Locale locale, TimeZone timeZone) { + if (null == locale) { + locale = Locale.getDefault(Locale.Category.FORMAT); + } + final SimpleDateFormat format = new SimpleDateFormat(pattern, locale); + if (null != timeZone) { + format.setTimeZone(timeZone); + } + format.setLenient(false); + return format; + } + + /** + * 获取时长单位简写 + * + * @param unit 单位 + * @return 单位简写名称 + * @since 5.7.16 + */ + public static String getShotName(TimeUnit unit) { + switch (unit) { + case NANOSECONDS: + return "ns"; + case MICROSECONDS: + return "μs"; + case MILLISECONDS: + return "ms"; + case SECONDS: + return "s"; + case MINUTES: + return "min"; + case HOURS: + return "h"; + default: + return unit.name().toLowerCase(); + } + } + + /** + * 检查两个时间段是否有时间重叠
+ * 重叠指两个时间段是否有交集,注意此方法时间段重合时如: + *
    + *
  • 此方法未纠正开始时间小于结束时间
  • + *
  • 当realStartTime和realEndTime或startTime和endTime相等时,退化为判断区间是否包含点
  • + *
  • 当realStartTime和realEndTime和startTime和endTime相等时,退化为判断点与点是否相等
  • + *
+ * See 准确的区间关系参考:艾伦区间代数 + * @param realStartTime 第一个时间段的开始时间 + * @param realEndTime 第一个时间段的结束时间 + * @param startTime 第二个时间段的开始时间 + * @param endTime 第二个时间段的结束时间 + * @return true 表示时间有重合或包含或相等 + * @since 5.7.22 + */ + public static boolean isOverlap(Date realStartTime, Date realEndTime, + Date startTime, Date endTime) { + + // x>b||a>y 无交集 + // 则有交集的逻辑为 !(x>b||a>y) + // 根据德摩根公式,可化简为 x<=b && a<=y 即 realStartTime<=endTime && startTime<=realEndTime + return realStartTime.compareTo(endTime) <=0 && startTime.compareTo(realEndTime) <= 0; + } + + /** + * 是否为本月最后一天 + * @param date {@link Date} + * @return 是否为本月最后一天 + * @since 5.8.9 + */ + public static boolean isLastDayOfMonth(Date date){ + return date(date).isLastDayOfMonth(); + } + + /** + * 获得本月的最后一天 + * @param date {@link Date} + * @return 天 + * @since 5.8.9 + */ + public static int getLastDayOfMonth(Date date){ + return date(date).getLastDayOfMonth(); + } + + // ------------------------------------------------------------------------ Private method start + + /** + * 标准化日期,默认处理以空格区分的日期时间格式,空格前为日期,空格后为时间:
+ * 将以下字符替换为"-" + * + *
+	 * "."
+	 * "/"
+	 * "年"
+	 * "月"
+	 * 
+ *

+ * 将以下字符去除 + * + *

+	 * "日"
+	 * 
+ *

+ * 将以下字符替换为":" + * + *

+	 * "时"
+	 * "分"
+	 * "秒"
+	 * 
+ *

+ * 当末位是":"时去除之(不存在毫秒时) + * + * @param dateStr 日期时间字符串 + * @return 格式化后的日期字符串 + */ + private static String normalize(CharSequence dateStr) { + if (StrUtil.isBlank(dateStr)) { + return StrUtil.str(dateStr); + } + + // 日期时间分开处理 + final List dateAndTime = StrUtil.splitTrim(dateStr, ' '); + final int size = dateAndTime.size(); + if (size < 1 || size > 2) { + // 非可被标准处理的格式 + return StrUtil.str(dateStr); + } + + final StringBuilder builder = StrUtil.builder(); + + // 日期部分("\"、"/"、"."、"年"、"月"都替换为"-") + String datePart = dateAndTime.get(0).replaceAll("[/.年月]", "-"); + datePart = StrUtil.removeSuffix(datePart, "日"); + builder.append(datePart); + + // 时间部分 + if (size == 2) { + builder.append(' '); + String timePart = dateAndTime.get(1).replaceAll("[时分秒]", ":"); + timePart = StrUtil.removeSuffix(timePart, ":"); + //将ISO8601中的逗号替换为. + timePart = timePart.replace(',', '.'); + builder.append(timePart); + } + + return builder.toString(); + } + // ------------------------------------------------------------------------ Private method end + + /** + * 如果日期中的毫秒部分超出3位,会导致秒数增加,因此只保留前三位 + * + * @param dateStr 日期字符串 + * @param before 毫秒部分的前一个字符 + * @param after 毫秒部分的后一个字符 + * @return 规范之后的毫秒部分 + */ + private static String normalizeMillSeconds(String dateStr, CharSequence before, CharSequence after) { + if (StrUtil.isBlank(after)) { + String millOrNaco = StrUtil.subPre(StrUtil.subAfter(dateStr, before, true), 3); + return StrUtil.subBefore(dateStr, before, true) + before + millOrNaco; + } + String millOrNaco = StrUtil.subPre(StrUtil.subBetween(dateStr, before, after), 3); + return StrUtil.subBefore(dateStr, before, true) + + before + + millOrNaco + after + StrUtil.subAfter(dateStr, after, true); + } +} diff --git a/src/main/java/cn/hutool/core/date/GroupTimeInterval.java b/src/main/java/cn/hutool/core/date/GroupTimeInterval.java new file mode 100644 index 0000000..34f5be1 --- /dev/null +++ b/src/main/java/cn/hutool/core/date/GroupTimeInterval.java @@ -0,0 +1,177 @@ +package cn.hutool.core.date; + +import cn.hutool.core.map.SafeConcurrentHashMap; +import cn.hutool.core.util.ObjectUtil; + +import java.io.Serializable; +import java.util.Map; + +/** + * 分组计时器
+ * 计算某几个过程花费的时间,精确到毫秒或纳秒 + * + * @author Looly + * @since 5.5.2 + */ +public class GroupTimeInterval implements Serializable { + private static final long serialVersionUID = 1L; + + private final boolean isNano; + protected final Map groupMap; + + /** + * 构造 + * + * @param isNano 是否使用纳秒计数,false则使用毫秒 + */ + public GroupTimeInterval(boolean isNano) { + this.isNano = isNano; + groupMap = new SafeConcurrentHashMap<>(); + } + + /** + * 清空所有定时记录 + * + * @return this + */ + public GroupTimeInterval clear(){ + this.groupMap.clear(); + return this; + } + + /** + * 开始计时并返回当前时间 + * + * @param id 分组ID + * @return 开始计时并返回当前时间 + */ + public long start(String id) { + final long time = getTime(); + this.groupMap.put(id, time); + return time; + } + + /** + * 重新计时并返回从开始到当前的持续时间秒
+ * 如果此分组下没有记录,则返回0; + * + * @param id 分组ID + * @return 重新计时并返回从开始到当前的持续时间 + */ + public long intervalRestart(String id) { + final long now = getTime(); + return now - ObjectUtil.defaultIfNull(this.groupMap.put(id, now), now); + } + + //----------------------------------------------------------- Interval + + /** + * 从开始到当前的间隔时间(毫秒数)
+ * 如果使用纳秒计时,返回纳秒差,否则返回毫秒差
+ * 如果分组下没有开始时间,返回{@code null} + * + * @param id 分组ID + * @return 从开始到当前的间隔时间(毫秒数) + */ + public long interval(String id) { + final Long lastTime = this.groupMap.get(id); + if (null == lastTime) { + return 0; + } + return getTime() - lastTime; + } + + /** + * 从开始到当前的间隔时间 + * + * @param id 分组ID + * @param dateUnit 时间单位 + * @return 从开始到当前的间隔时间(毫秒数) + */ + public long interval(String id, DateUnit dateUnit) { + final long intervalMs = isNano ? interval(id) / 1000000L : interval(id); + if (DateUnit.MS == dateUnit) { + return intervalMs; + } + return intervalMs / dateUnit.getMillis(); + } + + /** + * 从开始到当前的间隔时间(毫秒数) + * + * @param id 分组ID + * @return 从开始到当前的间隔时间(毫秒数) + */ + public long intervalMs(String id) { + return interval(id, DateUnit.MS); + } + + /** + * 从开始到当前的间隔秒数,取绝对值 + * + * @param id 分组ID + * @return 从开始到当前的间隔秒数,取绝对值 + */ + public long intervalSecond(String id) { + return interval(id, DateUnit.SECOND); + } + + /** + * 从开始到当前的间隔分钟数,取绝对值 + * + * @param id 分组ID + * @return 从开始到当前的间隔分钟数,取绝对值 + */ + public long intervalMinute(String id) { + return interval(id, DateUnit.MINUTE); + } + + /** + * 从开始到当前的间隔小时数,取绝对值 + * + * @param id 分组ID + * @return 从开始到当前的间隔小时数,取绝对值 + */ + public long intervalHour(String id) { + return interval(id, DateUnit.HOUR); + } + + /** + * 从开始到当前的间隔天数,取绝对值 + * + * @param id 分组ID + * @return 从开始到当前的间隔天数,取绝对值 + */ + public long intervalDay(String id) { + return interval(id, DateUnit.DAY); + } + + /** + * 从开始到当前的间隔周数,取绝对值 + * + * @param id 分组ID + * @return 从开始到当前的间隔周数,取绝对值 + */ + public long intervalWeek(String id) { + return interval(id, DateUnit.WEEK); + } + + /** + * 从开始到当前的间隔时间(毫秒数),返回XX天XX小时XX分XX秒XX毫秒 + * + * @param id 分组ID + * @return 从开始到当前的间隔时间(毫秒数) + */ + public String intervalPretty(String id) { + return DateUtil.formatBetween(intervalMs(id)); + } + + /** + * 获取时间的毫秒或纳秒数,纳秒非时间戳 + * + * @return 时间 + */ + private long getTime() { + return this.isNano ? System.nanoTime() : System.currentTimeMillis(); + } +} diff --git a/src/main/java/cn/hutool/core/date/LocalDateTimeUtil.java b/src/main/java/cn/hutool/core/date/LocalDateTimeUtil.java new file mode 100644 index 0000000..3b4916a --- /dev/null +++ b/src/main/java/cn/hutool/core/date/LocalDateTimeUtil.java @@ -0,0 +1,639 @@ +package cn.hutool.core.date; + +import cn.hutool.core.date.format.GlobalCustomFormat; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.ReUtil; +import cn.hutool.core.util.StrUtil; + +import java.time.*; +import java.time.chrono.ChronoLocalDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeFormatterBuilder; +import java.time.temporal.*; +import java.util.Date; +import java.util.TimeZone; + +/** + * JDK8+中的{@link LocalDateTime} 工具类封装 + * + * @author looly + * @see DateUtil java7和以下版本,使用Date工具类 + * @see DatePattern 常用格式工具类 + * @since 5.3.9 + */ +public class LocalDateTimeUtil { + + /** + * 当前时间,默认时区 + * + * @return {@link LocalDateTime} + */ + public static LocalDateTime now() { + return LocalDateTime.now(); + } + + /** + * {@link Instant}转{@link LocalDateTime},使用默认时区 + * + * @param instant {@link Instant} + * @return {@link LocalDateTime} + */ + public static LocalDateTime of(Instant instant) { + return of(instant, ZoneId.systemDefault()); + } + + /** + * {@link Instant}转{@link LocalDateTime},使用UTC时区 + * + * @param instant {@link Instant} + * @return {@link LocalDateTime} + */ + public static LocalDateTime ofUTC(Instant instant) { + return of(instant, ZoneId.of("UTC")); + } + + /** + * {@link ZonedDateTime}转{@link LocalDateTime} + * + * @param zonedDateTime {@link ZonedDateTime} + * @return {@link LocalDateTime} + */ + public static LocalDateTime of(ZonedDateTime zonedDateTime) { + if (null == zonedDateTime) { + return null; + } + return zonedDateTime.toLocalDateTime(); + } + + /** + * {@link Instant}转{@link LocalDateTime} + * + * @param instant {@link Instant} + * @param zoneId 时区 + * @return {@link LocalDateTime} + */ + public static LocalDateTime of(Instant instant, ZoneId zoneId) { + if (null == instant) { + return null; + } + + return LocalDateTime.ofInstant(instant, ObjectUtil.defaultIfNull(zoneId, ZoneId::systemDefault)); + } + + /** + * {@link Instant}转{@link LocalDateTime} + * + * @param instant {@link Instant} + * @param timeZone 时区 + * @return {@link LocalDateTime} + */ + public static LocalDateTime of(Instant instant, TimeZone timeZone) { + if (null == instant) { + return null; + } + + return of(instant, ObjectUtil.defaultIfNull(timeZone, TimeZone::getDefault).toZoneId()); + } + + /** + * 毫秒转{@link LocalDateTime},使用默认时区 + * + *

注意:此方法使用默认时区,如果非UTC,会产生时间偏移

+ * + * @param epochMilli 从1970-01-01T00:00:00Z开始计数的毫秒数 + * @return {@link LocalDateTime} + */ + public static LocalDateTime of(long epochMilli) { + return of(Instant.ofEpochMilli(epochMilli)); + } + + /** + * 毫秒转{@link LocalDateTime},使用UTC时区 + * + * @param epochMilli 从1970-01-01T00:00:00Z开始计数的毫秒数 + * @return {@link LocalDateTime} + */ + public static LocalDateTime ofUTC(long epochMilli) { + return ofUTC(Instant.ofEpochMilli(epochMilli)); + } + + /** + * 毫秒转{@link LocalDateTime},根据时区不同,结果会产生时间偏移 + * + * @param epochMilli 从1970-01-01T00:00:00Z开始计数的毫秒数 + * @param zoneId 时区 + * @return {@link LocalDateTime} + */ + public static LocalDateTime of(long epochMilli, ZoneId zoneId) { + return of(Instant.ofEpochMilli(epochMilli), zoneId); + } + + /** + * 毫秒转{@link LocalDateTime},结果会产生时间偏移 + * + * @param epochMilli 从1970-01-01T00:00:00Z开始计数的毫秒数 + * @param timeZone 时区 + * @return {@link LocalDateTime} + */ + public static LocalDateTime of(long epochMilli, TimeZone timeZone) { + return of(Instant.ofEpochMilli(epochMilli), timeZone); + } + + /** + * {@link Date}转{@link LocalDateTime},使用默认时区 + * + * @param date Date对象 + * @return {@link LocalDateTime} + */ + public static LocalDateTime of(Date date) { + if (null == date) { + return null; + } + + if (date instanceof DateTime) { + return of(date.toInstant(), ((DateTime) date).getZoneId()); + } + return of(date.toInstant()); + } + + /** + * {@link TemporalAccessor}转{@link LocalDateTime},使用默认时区 + * + * @param temporalAccessor {@link TemporalAccessor} + * @return {@link LocalDateTime} + */ + public static LocalDateTime of(TemporalAccessor temporalAccessor) { + if (null == temporalAccessor) { + return null; + } + + if (temporalAccessor instanceof LocalDate) { + return ((LocalDate) temporalAccessor).atStartOfDay(); + } else if(temporalAccessor instanceof Instant){ + return LocalDateTime.ofInstant((Instant) temporalAccessor, ZoneId.systemDefault()); + } else if(temporalAccessor instanceof ZonedDateTime){ + return ((ZonedDateTime)temporalAccessor).toLocalDateTime(); + } + + return LocalDateTime.of( + TemporalAccessorUtil.get(temporalAccessor, ChronoField.YEAR), + TemporalAccessorUtil.get(temporalAccessor, ChronoField.MONTH_OF_YEAR), + TemporalAccessorUtil.get(temporalAccessor, ChronoField.DAY_OF_MONTH), + TemporalAccessorUtil.get(temporalAccessor, ChronoField.HOUR_OF_DAY), + TemporalAccessorUtil.get(temporalAccessor, ChronoField.MINUTE_OF_HOUR), + TemporalAccessorUtil.get(temporalAccessor, ChronoField.SECOND_OF_MINUTE), + TemporalAccessorUtil.get(temporalAccessor, ChronoField.NANO_OF_SECOND) + ); + } + + /** + * {@link TemporalAccessor}转{@link LocalDate},使用默认时区 + * + * @param temporalAccessor {@link TemporalAccessor} + * @return {@link LocalDate} + * @since 5.3.10 + */ + public static LocalDate ofDate(TemporalAccessor temporalAccessor) { + if (null == temporalAccessor) { + return null; + } + + if (temporalAccessor instanceof LocalDateTime) { + return ((LocalDateTime) temporalAccessor).toLocalDate(); + } else if(temporalAccessor instanceof Instant){ + return of(temporalAccessor).toLocalDate(); + } + + return LocalDate.of( + TemporalAccessorUtil.get(temporalAccessor, ChronoField.YEAR), + TemporalAccessorUtil.get(temporalAccessor, ChronoField.MONTH_OF_YEAR), + TemporalAccessorUtil.get(temporalAccessor, ChronoField.DAY_OF_MONTH) + ); + } + + /** + * 解析日期时间字符串为{@link LocalDateTime},仅支持yyyy-MM-dd'T'HH:mm:ss格式,例如:2007-12-03T10:15:30
+ * 即{@link DateTimeFormatter#ISO_LOCAL_DATE_TIME} + * + * @param text 日期时间字符串 + * @return {@link LocalDateTime} + */ + public static LocalDateTime parse(CharSequence text) { + return parse(text, (DateTimeFormatter) null); + } + + /** + * 解析日期时间字符串为{@link LocalDateTime},格式支持日期时间、日期、时间
+ * 如果formatter为{code null},则使用{@link DateTimeFormatter#ISO_LOCAL_DATE_TIME} + * + * @param text 日期时间字符串 + * @param formatter 日期格式化器,预定义的格式见:{@link DateTimeFormatter} + * @return {@link LocalDateTime} + */ + public static LocalDateTime parse(CharSequence text, DateTimeFormatter formatter) { + if (StrUtil.isBlank(text)) { + return null; + } + if (null == formatter) { + return LocalDateTime.parse(text); + } + + return of(formatter.parse(text)); + } + + /** + * 解析日期时间字符串为{@link LocalDateTime} + * + * @param text 日期时间字符串 + * @param format 日期格式,类似于yyyy-MM-dd HH:mm:ss,SSS + * @return {@link LocalDateTime} + */ + public static LocalDateTime parse(CharSequence text, String format) { + if (StrUtil.isBlank(text)) { + return null; + } + + if (GlobalCustomFormat.isCustomFormat(format)) { + return of(GlobalCustomFormat.parse(text, format)); + } + + DateTimeFormatter formatter = null; + if (StrUtil.isNotBlank(format)) { + // 修复yyyyMMddHHmmssSSS格式不能解析的问题 + // fix issue#1082 + //see https://stackoverflow.com/questions/22588051/is-java-time-failing-to-parse-fraction-of-second + // jdk8 bug at: https://bugs.openjdk.java.net/browse/JDK-8031085 + if (StrUtil.startWithIgnoreEquals(format, DatePattern.PURE_DATETIME_PATTERN)) { + final String fraction = StrUtil.removePrefix(format, DatePattern.PURE_DATETIME_PATTERN); + if (ReUtil.isMatch("[S]{1,2}", fraction)) { + //将yyyyMMddHHmmssS、yyyyMMddHHmmssSS的日期统一替换为yyyyMMddHHmmssSSS格式,用0补 + text += StrUtil.repeat('0', 3 - fraction.length()); + } + formatter = new DateTimeFormatterBuilder() + .appendPattern(DatePattern.PURE_DATETIME_PATTERN) + .appendValue(ChronoField.MILLI_OF_SECOND, 3) + .toFormatter(); + } else { + formatter = DateTimeFormatter.ofPattern(format); + } + } + + return parse(text, formatter); + } + + /** + * 解析日期时间字符串为{@link LocalDate},仅支持yyyy-MM-dd'T'HH:mm:ss格式,例如:2007-12-03T10:15:30 + * + * @param text 日期时间字符串 + * @return {@link LocalDate} + * @since 5.3.10 + */ + public static LocalDate parseDate(CharSequence text) { + return parseDate(text, (DateTimeFormatter) null); + } + + /** + * 解析日期时间字符串为{@link LocalDate},格式支持日期 + * + * @param text 日期时间字符串 + * @param formatter 日期格式化器,预定义的格式见:{@link DateTimeFormatter} + * @return {@link LocalDate} + * @since 5.3.10 + */ + public static LocalDate parseDate(CharSequence text, DateTimeFormatter formatter) { + if (null == text) { + return null; + } + if (null == formatter) { + return LocalDate.parse(text); + } + + return ofDate(formatter.parse(text)); + } + + /** + * 解析日期字符串为{@link LocalDate} + * + * @param text 日期字符串 + * @param format 日期格式,类似于yyyy-MM-dd + * @return {@link LocalDateTime} + */ + public static LocalDate parseDate(CharSequence text, String format) { + if (null == text) { + return null; + } + return parseDate(text, DateTimeFormatter.ofPattern(format)); + } + + /** + * 格式化日期时间为yyyy-MM-dd HH:mm:ss格式 + * + * @param time {@link LocalDateTime} + * @return 格式化后的字符串 + * @since 5.3.11 + */ + public static String formatNormal(LocalDateTime time) { + return format(time, DatePattern.NORM_DATETIME_FORMATTER); + } + + /** + * 格式化日期时间为指定格式 + * + * @param time {@link LocalDateTime} + * @param formatter 日期格式化器,预定义的格式见:{@link DateTimeFormatter} + * @return 格式化后的字符串 + */ + public static String format(LocalDateTime time, DateTimeFormatter formatter) { + return TemporalAccessorUtil.format(time, formatter); + } + + /** + * 格式化日期时间为指定格式 + * + * @param time {@link LocalDateTime} + * @param format 日期格式,类似于yyyy-MM-dd HH:mm:ss,SSS + * @return 格式化后的字符串 + */ + public static String format(LocalDateTime time, String format) { + return TemporalAccessorUtil.format(time, format); + } + + /** + * 格式化日期时间为yyyy-MM-dd格式 + * + * @param date {@link LocalDate} + * @return 格式化后的字符串 + * @since 5.3.11 + */ + public static String formatNormal(LocalDate date) { + return format(date, DatePattern.NORM_DATE_FORMATTER); + } + + /** + * 格式化日期时间为指定格式 + * + * @param date {@link LocalDate} + * @param formatter 日期格式化器,预定义的格式见:{@link DateTimeFormatter}; 常量如: {@link DatePattern#NORM_DATE_FORMATTER}, {@link DatePattern#NORM_DATETIME_FORMATTER} + * @return 格式化后的字符串 + * @since 5.3.10 + */ + public static String format(LocalDate date, DateTimeFormatter formatter) { + return TemporalAccessorUtil.format(date, formatter); + } + + /** + * 格式化日期时间为指定格式 + * + * @param date {@link LocalDate} + * @param format 日期格式,类似于yyyy-MM-dd, 常量如 {@link DatePattern#NORM_DATE_PATTERN}, {@link DatePattern#NORM_DATETIME_PATTERN} + * @return 格式化后的字符串 + * @since 5.3.10 + */ + public static String format(LocalDate date, String format) { + if (null == date) { + return null; + } + return format(date, DateTimeFormatter.ofPattern(format)); + } + + /** + * 日期偏移,根据field不同加不同值(偏移会修改传入的对象) + * + * @param time {@link LocalDateTime} + * @param number 偏移量,正数为向后偏移,负数为向前偏移 + * @param field 偏移单位,见{@link ChronoUnit},不能为null + * @return 偏移后的日期时间 + */ + public static LocalDateTime offset(LocalDateTime time, long number, TemporalUnit field) { + return TemporalUtil.offset(time, number, field); + } + + /** + * 获取两个日期的差,如果结束时间早于开始时间,获取结果为负。 + *

+ * 返回结果为{@link Duration}对象,通过调用toXXX方法返回相差单位 + * + * @param startTimeInclude 开始时间(包含) + * @param endTimeExclude 结束时间(不包含) + * @return 时间差 {@link Duration}对象 + * @see TemporalUtil#between(Temporal, Temporal) + */ + public static Duration between(LocalDateTime startTimeInclude, LocalDateTime endTimeExclude) { + return TemporalUtil.between(startTimeInclude, endTimeExclude); + } + + /** + * 获取两个日期的差,如果结束时间早于开始时间,获取结果为负。 + *

+ * 返回结果为时间差的long值 + * + * @param startTimeInclude 开始时间(包括) + * @param endTimeExclude 结束时间(不包括) + * @param unit 时间差单位 + * @return 时间差 + * @since 5.4.5 + */ + public static long between(LocalDateTime startTimeInclude, LocalDateTime endTimeExclude, ChronoUnit unit) { + return TemporalUtil.between(startTimeInclude, endTimeExclude, unit); + } + + /** + * 获取两个日期的表象时间差,如果结束时间早于开始时间,获取结果为负。 + *

+ * 比如2011年2月1日,和2021年8月11日,日相差了10天,月相差6月 + * + * @param startTimeInclude 开始时间(包括) + * @param endTimeExclude 结束时间(不包括) + * @return 时间差 + * @since 5.4.5 + */ + public static Period betweenPeriod(LocalDate startTimeInclude, LocalDate endTimeExclude) { + return Period.between(startTimeInclude, endTimeExclude); + } + + /** + * 修改为一天的开始时间,例如:2020-02-02 00:00:00,000 + * + * @param time 日期时间 + * @return 一天的开始时间 + */ + public static LocalDateTime beginOfDay(LocalDateTime time) { + return time.with(LocalTime.MIN); + } + + /** + * 修改为一天的结束时间,例如:2020-02-02 23:59:59,999 + * + * @param time 日期时间 + * @return 一天的结束时间 + */ + public static LocalDateTime endOfDay(LocalDateTime time) { + return endOfDay(time, false); + } + + /** + * 修改为一天的结束时间,例如: + *

    + *
  • 毫秒不归零:2020-02-02 23:59:59,999
  • + *
  • 毫秒归零:2020-02-02 23:59:59,000
  • + *
+ * + * @param time 日期时间 + * @param truncateMillisecond 是否毫秒归零 + * @return 一天的结束时间 + * @since 5.7.18 + */ + public static LocalDateTime endOfDay(LocalDateTime time, boolean truncateMillisecond) { + if (truncateMillisecond) { + return time.with(LocalTime.of(23, 59, 59)); + } + return time.with(LocalTime.MAX); + } + + /** + * {@link TemporalAccessor}转换为 时间戳(从1970-01-01T00:00:00Z开始的毫秒数) + * + * @param temporalAccessor Date对象 + * @return {@link Instant}对象 + * @see TemporalAccessorUtil#toEpochMilli(TemporalAccessor) + * @since 5.4.1 + */ + public static long toEpochMilli(TemporalAccessor temporalAccessor) { + return TemporalAccessorUtil.toEpochMilli(temporalAccessor); + } + + /** + * 是否为周末(周六或周日) + * + * @param localDateTime 判定的日期{@link LocalDateTime} + * @return 是否为周末(周六或周日) + * @since 5.7.6 + */ + public static boolean isWeekend(LocalDateTime localDateTime) { + return isWeekend(localDateTime.toLocalDate()); + } + + /** + * 是否为周末(周六或周日) + * + * @param localDate 判定的日期{@link LocalDate} + * @return 是否为周末(周六或周日) + * @since 5.7.6 + */ + public static boolean isWeekend(LocalDate localDate) { + final DayOfWeek dayOfWeek = localDate.getDayOfWeek(); + return DayOfWeek.SATURDAY == dayOfWeek || DayOfWeek.SUNDAY == dayOfWeek; + } + + /** + * 获取{@link LocalDate}对应的星期值 + * + * @param localDate 日期{@link LocalDate} + * @return {@link Week} + * @since 5.7.14 + */ + public static Week dayOfWeek(LocalDate localDate) { + return Week.of(localDate.getDayOfWeek()); + } + + /** + * 检查两个时间段是否有时间重叠
+ * 重叠指两个时间段是否有交集,注意此方法时间段重合时如: + *
    + *
  • 此方法未纠正开始时间小于结束时间
  • + *
  • 当realStartTime和realEndTime或startTime和endTime相等时,退化为判断区间是否包含点
  • + *
  • 当realStartTime和realEndTime和startTime和endTime相等时,退化为判断点与点是否相等
  • + *
+ * See 准确的区间关系参考:艾伦区间代数 + * @param realStartTime 第一个时间段的开始时间 + * @param realEndTime 第一个时间段的结束时间 + * @param startTime 第二个时间段的开始时间 + * @param endTime 第二个时间段的结束时间 + * @return true 表示时间有重合或包含或相等 + * @since 5.7.20 + */ + public static boolean isOverlap(ChronoLocalDateTime realStartTime, ChronoLocalDateTime realEndTime, + ChronoLocalDateTime startTime, ChronoLocalDateTime endTime) { + + // x>b||a>y 无交集 + // 则有交集的逻辑为 !(x>b||a>y) + // 根据德摩根公式,可化简为 x<=b && a<=y 即 realStartTime<=endTime && startTime<=realEndTime + return realStartTime.compareTo(endTime) <=0 && startTime.compareTo(realEndTime) <= 0; + } + + /** + * 获得指定日期是所在年份的第几周,如: + *
    + *
  • 如果一年的第一天是星期一,则第一周从第一天开始,没有零周
  • + *
  • 如果一年的第二天是星期一,则第一周从第二天开始,而第一天在零周
  • + *
  • 如果一年中的第4天是星期一,则第1周从第4周开始,第1至第3周在零周开始
  • + *
  • 如果一年中的第5天是星期一,则第二周从第5周开始,第1至第4周在第1周
  • + *
+ * + * + * @param date 日期({@link LocalDate} 或者 {@link LocalDateTime}等) + * @return 所在年的第几周 + * @since 5.7.21 + */ + public static int weekOfYear(TemporalAccessor date){ + return TemporalAccessorUtil.get(date, WeekFields.ISO.weekOfYear()); + } + + /** + * 比较两个日期是否为同一天 + * + * @param date1 日期1 + * @param date2 日期2 + * @return 是否为同一天 + * @since 5.8.5 + */ + public static boolean isSameDay(final LocalDateTime date1, final LocalDateTime date2) { + return date1 != null && date2 != null && isSameDay(date1.toLocalDate(), date2.toLocalDate()); + } + + /** + * 比较两个日期是否为同一天 + * + * @param date1 日期1 + * @param date2 日期2 + * @return 是否为同一天 + * @since 5.8.5 + */ + public static boolean isSameDay(final LocalDate date1, final LocalDate date2) { + return date1 != null && date2 != null && date1.isEqual(date2); + } + + /** + * 当前日期是否在日期指定范围内
+ * 起始日期和结束日期可以互换 + * + * @param date 被检查的日期 + * @param beginDate 起始日期(包含) + * @param endDate 结束日期(包含) + * @return 是否在范围内 + * @since 5.8.5 + */ + public static boolean isIn(ChronoLocalDateTime date, ChronoLocalDateTime beginDate, ChronoLocalDateTime endDate) { + return TemporalAccessorUtil.isIn(date, beginDate, endDate); + } + + /** + * 判断当前时间(默认时区)是否在指定范围内
+ * 起始时间和结束时间可以互换
+ * 通过includeBegin, includeEnd参数控制时间范围区间是否为开区间,例如:传入参数:includeBegin=true, includeEnd=false, + * 则本方法会判断 date ∈ (beginDate, endDate] 是否成立 + * + * @param date 被判定的日期 + * @param beginDate 起始时间(包含) + * @param endDate 结束时间(包含) + * @param includeBegin 时间范围是否包含起始时间 + * @param includeEnd 时间范围是否包含结束时间 + * @return 是否在范围内 + * @author FengBaoheng + * @since 5.8.6 + */ + public static boolean isIn(ChronoLocalDateTime date, ChronoLocalDateTime beginDate, + ChronoLocalDateTime endDate, boolean includeBegin, boolean includeEnd) { + return TemporalAccessorUtil.isIn(date, beginDate, endDate, includeBegin, includeEnd); + } +} diff --git a/src/main/java/cn/hutool/core/date/Month.java b/src/main/java/cn/hutool/core/date/Month.java new file mode 100644 index 0000000..95d71c0 --- /dev/null +++ b/src/main/java/cn/hutool/core/date/Month.java @@ -0,0 +1,246 @@ +package cn.hutool.core.date; + +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.ArrayUtil; + +import java.time.format.TextStyle; +import java.util.Calendar; +import java.util.Locale; + +/** + * 月份枚举
+ * 与Calendar中的月份int值对应 + * + * @author Looly + * @see Calendar#JANUARY + * @see Calendar#FEBRUARY + * @see Calendar#MARCH + * @see Calendar#APRIL + * @see Calendar#MAY + * @see Calendar#JUNE + * @see Calendar#JULY + * @see Calendar#AUGUST + * @see Calendar#SEPTEMBER + * @see Calendar#OCTOBER + * @see Calendar#NOVEMBER + * @see Calendar#DECEMBER + * @see Calendar#UNDECIMBER + */ +public enum Month { + /** + * 一月 + */ + JANUARY(Calendar.JANUARY), + /** + * 二月 + */ + FEBRUARY(Calendar.FEBRUARY), + /** + * 三月 + */ + MARCH(Calendar.MARCH), + /** + * 四月 + */ + APRIL(Calendar.APRIL), + /** + * 五月 + */ + MAY(Calendar.MAY), + /** + * 六月 + */ + JUNE(Calendar.JUNE), + /** + * 七月 + */ + JULY(Calendar.JULY), + /** + * 八月 + */ + AUGUST(Calendar.AUGUST), + /** + * 九月 + */ + SEPTEMBER(Calendar.SEPTEMBER), + /** + * 十月 + */ + OCTOBER(Calendar.OCTOBER), + /** + * 十一月 + */ + NOVEMBER(Calendar.NOVEMBER), + /** + * 十二月 + */ + DECEMBER(Calendar.DECEMBER), + /** + * 十三月,仅用于农历 + */ + UNDECIMBER(Calendar.UNDECIMBER); + + // --------------------------------------------------------------- + /** + * Months aliases. + */ + private static final String[] ALIASES = {"jan", "feb", "mar", "apr", "may", "jun", "jul", "aug", "sep", "oct", "nov", "dec"}; + private static final Month[] ENUMS = Month.values(); + + /** + * 对应值,见{@link Calendar} + */ + private final int value; + + /** + * 构造 + * + * @param value 对应值,见{@link Calendar} + */ + Month(int value) { + this.value = value; + } + + /** + * 获取{@link Calendar}中的对应值
+ * 此值从0开始,即0表示一月 + * + * @return {@link Calendar}中的对应月份值,从0开始计数 + */ + public int getValue() { + return this.value; + } + + /** + * 获取月份值,此值与{@link java.time.Month}对应
+ * 此值从1开始,即1表示一月 + * + * @return 月份值,对应{@link java.time.Month},从1开始计数 + * @since 5.7.21 + */ + public int getValueBaseOne() { + Assert.isFalse(this == UNDECIMBER, "Unsupported UNDECIMBER Field"); + return getValue() + 1; + } + + /** + * 获取此月份最后一天的值,不支持的月份(例如UNDECIMBER)返回-1 + * + * @param isLeapYear 是否闰年 + * @return 此月份最后一天的值 + */ + public int getLastDay(boolean isLeapYear) { + switch (this) { + case FEBRUARY: + return isLeapYear ? 29 : 28; + case APRIL: + case JUNE: + case SEPTEMBER: + case NOVEMBER: + return 30; + default: + return 31; + } + } + + /** + * 将 {@link Calendar}月份相关值转换为Month枚举对象
+ * 未找到返回{@code null} + * + * @param calendarMonthIntValue Calendar中关于Month的int值,从0开始 + * @return Month + * @see Calendar#JANUARY + * @see Calendar#FEBRUARY + * @see Calendar#MARCH + * @see Calendar#APRIL + * @see Calendar#MAY + * @see Calendar#JUNE + * @see Calendar#JULY + * @see Calendar#AUGUST + * @see Calendar#SEPTEMBER + * @see Calendar#OCTOBER + * @see Calendar#NOVEMBER + * @see Calendar#DECEMBER + * @see Calendar#UNDECIMBER + */ + public static Month of(int calendarMonthIntValue) { + if (calendarMonthIntValue >= ENUMS.length || calendarMonthIntValue < 0) { + return null; + } + return ENUMS[calendarMonthIntValue]; + } + + /** + * 解析别名为Month对象,别名如:jan或者JANUARY,不区分大小写 + * + * @param name 别名值 + * @return 月份枚举Month,非空 + * @throws IllegalArgumentException 如果别名无对应的枚举,抛出此异常 + * @since 5.8.0 + */ + public static Month of(String name) throws IllegalArgumentException { + Assert.notBlank(name); + Month of = of(ArrayUtil.indexOfIgnoreCase(ALIASES, name)); + if (null == of) { + of = Month.valueOf(name.toUpperCase()); + } + return of; + } + + /** + * {@link java.time.Month}转换为Month对象 + * @param month {@link java.time.Month} + * @return Month + * @since 5.8.0 + */ + public static Month of(java.time.Month month){ + return of(month.ordinal()); + } + + /** + * 获得指定月的最后一天 + * + * @param month 月份,从0开始 + * @param isLeapYear 是否为闰年,闰年只对二月有影响 + * @return 最后一天,可能为28,29,30,31 + * @since 5.4.7 + */ + public static int getLastDay(int month, boolean isLeapYear) { + final Month of = of(month); + Assert.notNull(of, "Invalid Month base 0: " + month); + return of.getLastDay(isLeapYear); + } + + /** + * 转换为{@link java.time.Month} + * + * @return {@link java.time.Month} + * @since 5.7.21 + */ + public java.time.Month toJdkMonth() { + return java.time.Month.of(getValueBaseOne()); + } + + /** + * 获取显示名称 + * + * @param style 名称风格 + * @return 显示名称 + * @since 5.8.0 + */ + public String getDisplayName(TextStyle style) { + return getDisplayName(style, Locale.getDefault()); + } + + /** + * 获取显示名称 + * + * @param style 名称风格 + * @param locale {@link Locale} + * @return 显示名称 + * @since 5.8.0 + */ + public String getDisplayName(TextStyle style, Locale locale) { + return toJdkMonth().getDisplayName(style, locale); + } +} diff --git a/src/main/java/cn/hutool/core/date/Quarter.java b/src/main/java/cn/hutool/core/date/Quarter.java new file mode 100644 index 0000000..3a09851 --- /dev/null +++ b/src/main/java/cn/hutool/core/date/Quarter.java @@ -0,0 +1,61 @@ +package cn.hutool.core.date; + +/** + * 季度枚举 + * + * @see #Q1 + * @see #Q2 + * @see #Q3 + * @see #Q4 + * + * @author zhfish(https://github.com/zhfish) + * + */ +public enum Quarter { + + /** 第一季度 */ + Q1(1), + /** 第二季度 */ + Q2(2), + /** 第三季度 */ + Q3(3), + /** 第四季度 */ + Q4(4); + + // --------------------------------------------------------------- + private final int value; + + Quarter(int value) { + this.value = value; + } + + public int getValue() { + return this.value; + } + + /** + * 将 季度int转换为Season枚举对象
+ * + * @see #Q1 + * @see #Q2 + * @see #Q3 + * @see #Q4 + * + * @param intValue 季度int表示 + * @return {@link Quarter} + */ + public static Quarter of(int intValue) { + switch (intValue) { + case 1: + return Q1; + case 2: + return Q2; + case 3: + return Q3; + case 4: + return Q4; + default: + return null; + } + } +} diff --git a/src/main/java/cn/hutool/core/date/StopWatch.java b/src/main/java/cn/hutool/core/date/StopWatch.java new file mode 100644 index 0000000..e83bd25 --- /dev/null +++ b/src/main/java/cn/hutool/core/date/StopWatch.java @@ -0,0 +1,485 @@ +package cn.hutool.core.date; + +import cn.hutool.core.io.FileUtil; +import cn.hutool.core.util.StrUtil; + +import java.text.NumberFormat; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; + +/** + * 秒表封装
+ * 此工具用于存储一组任务的耗时时间,并一次性打印对比。
+ * 比如:我们可以记录多段代码耗时时间,然后一次性打印(StopWatch提供了一个prettyString()函数用于按照指定格式打印出耗时) + * + *

+ * 此工具来自:https://github.com/spring-projects/spring-framework/blob/master/spring-core/src/main/java/org/springframework/util/StopWatch.java + * + *

+ * 使用方法如下: + * + *

+ * StopWatch stopWatch = new StopWatch("任务名称");
+ *
+ * // 任务1
+ * stopWatch.start("任务一");
+ * Thread.sleep(1000);
+ * stopWatch.stop();
+ *
+ * // 任务2
+ * stopWatch.start("任务二");
+ * Thread.sleep(2000);
+ * stopWatch.stop();
+ *
+ * // 打印出耗时
+ * Console.log(stopWatch.prettyPrint());
+ *
+ * 
+ * + * @author Spring Framework, Looly + * @since 4.6.6 + */ +public class StopWatch { + + /** + * 创建计时任务(秒表) + * + * @param id 用于标识秒表的唯一ID + * @return StopWatch + * @since 5.5.2 + */ + public static StopWatch create(String id) { + return new StopWatch(id); + } + + /** + * 秒表唯一标识,用于多个秒表对象的区分 + */ + private final String id; + private List taskList; + + /** + * 任务名称 + */ + private String currentTaskName; + /** + * 开始时间 + */ + private long startTimeNanos; + + /** + * 最后一次任务对象 + */ + private TaskInfo lastTaskInfo; + /** + * 总任务数 + */ + private int taskCount; + /** + * 总运行时间 + */ + private long totalTimeNanos; + // ------------------------------------------------------------------------------------------- Constructor start + + /** + * 构造,不启动任何任务 + */ + public StopWatch() { + this(StrUtil.EMPTY); + } + + /** + * 构造,不启动任何任务 + * + * @param id 用于标识秒表的唯一ID + */ + public StopWatch(String id) { + this(id, true); + } + + /** + * 构造,不启动任何任务 + * + * @param id 用于标识秒表的唯一ID + * @param keepTaskList 是否在停止后保留任务,{@code false} 表示停止运行后不保留任务 + */ + public StopWatch(String id, boolean keepTaskList) { + this.id = id; + if (keepTaskList) { + this.taskList = new ArrayList<>(); + } + } + // ------------------------------------------------------------------------------------------- Constructor end + + /** + * 获取StopWatch 的ID,用于多个秒表对象的区分 + * + * @return the ID 默认为空字符串 + * @see #StopWatch(String) + */ + public String getId() { + return this.id; + } + + /** + * 设置是否在停止后保留任务,{@code false} 表示停止运行后不保留任务 + * + * @param keepTaskList 是否在停止后保留任务 + */ + public void setKeepTaskList(boolean keepTaskList) { + if (keepTaskList) { + if (null == this.taskList) { + this.taskList = new ArrayList<>(); + } + } else { + this.taskList = null; + } + } + + /** + * 开始默认的新任务 + * + * @throws IllegalStateException 前一个任务没有结束 + */ + public void start() throws IllegalStateException { + start(StrUtil.EMPTY); + } + + /** + * 开始指定名称的新任务 + * + * @param taskName 新开始的任务名称 + * @throws IllegalStateException 前一个任务没有结束 + */ + public void start(String taskName) throws IllegalStateException { + if (null != this.currentTaskName) { + throw new IllegalStateException("Can't start StopWatch: it's already running"); + } + this.currentTaskName = taskName; + this.startTimeNanos = System.nanoTime(); + } + + /** + * 停止当前任务 + * + * @throws IllegalStateException 任务没有开始 + */ + public void stop() throws IllegalStateException { + if (null == this.currentTaskName) { + throw new IllegalStateException("Can't stop StopWatch: it's not running"); + } + + final long lastTime = System.nanoTime() - this.startTimeNanos; + this.totalTimeNanos += lastTime; + this.lastTaskInfo = new TaskInfo(this.currentTaskName, lastTime); + if (null != this.taskList) { + this.taskList.add(this.lastTaskInfo); + } + ++this.taskCount; + this.currentTaskName = null; + } + + /** + * 检查是否有正在运行的任务 + * + * @return 是否有正在运行的任务 + * @see #currentTaskName() + */ + public boolean isRunning() { + return (this.currentTaskName != null); + } + + /** + * 获取当前任务名,{@code null} 表示无任务 + * + * @return 当前任务名,{@code null} 表示无任务 + * @see #isRunning() + */ + public String currentTaskName() { + return this.currentTaskName; + } + + /** + * 获取最后任务的花费时间(纳秒) + * + * @return 任务的花费时间(纳秒) + * @throws IllegalStateException 无任务 + */ + public long getLastTaskTimeNanos() throws IllegalStateException { + if (this.lastTaskInfo == null) { + throw new IllegalStateException("No tasks run: can't get last task interval"); + } + return this.lastTaskInfo.getTimeNanos(); + } + + /** + * 获取最后任务的花费时间(毫秒) + * + * @return 任务的花费时间(毫秒) + * @throws IllegalStateException 无任务 + */ + public long getLastTaskTimeMillis() throws IllegalStateException { + if (this.lastTaskInfo == null) { + throw new IllegalStateException("No tasks run: can't get last task interval"); + } + return this.lastTaskInfo.getTimeMillis(); + } + + /** + * 获取最后的任务名 + * + * @return 任务名 + * @throws IllegalStateException 无任务 + */ + public String getLastTaskName() throws IllegalStateException { + if (this.lastTaskInfo == null) { + throw new IllegalStateException("No tasks run: can't get last task name"); + } + return this.lastTaskInfo.getTaskName(); + } + + /** + * 获取最后的任务对象 + * + * @return {@link TaskInfo} 任务对象,包括任务名和花费时间 + * @throws IllegalStateException 无任务 + */ + public TaskInfo getLastTaskInfo() throws IllegalStateException { + if (this.lastTaskInfo == null) { + throw new IllegalStateException("No tasks run: can't get last task info"); + } + return this.lastTaskInfo; + } + + /** + * 获取所有任务的总花费时间 + * + * @param unit 时间单位,{@code null}表示默认{@link TimeUnit#NANOSECONDS} + * @return 花费时间 + * @since 5.7.16 + */ + public long getTotal(TimeUnit unit){ + return unit.convert(this.totalTimeNanos, TimeUnit.NANOSECONDS); + } + + /** + * 获取所有任务的总花费时间(纳秒) + * + * @return 所有任务的总花费时间(纳秒) + * @see #getTotalTimeMillis() + * @see #getTotalTimeSeconds() + */ + public long getTotalTimeNanos() { + return this.totalTimeNanos; + } + + /** + * 获取所有任务的总花费时间(毫秒) + * + * @return 所有任务的总花费时间(毫秒) + * @see #getTotalTimeNanos() + * @see #getTotalTimeSeconds() + */ + public long getTotalTimeMillis() { + return getTotal(TimeUnit.MILLISECONDS); + } + + /** + * 获取所有任务的总花费时间(秒) + * + * @return 所有任务的总花费时间(秒) + * @see #getTotalTimeNanos() + * @see #getTotalTimeMillis() + */ + public double getTotalTimeSeconds() { + return DateUtil.nanosToSeconds(this.totalTimeNanos); + } + + /** + * 获取任务数 + * + * @return 任务数 + */ + public int getTaskCount() { + return this.taskCount; + } + + /** + * 获取任务列表 + * + * @return 任务列表 + */ + public TaskInfo[] getTaskInfo() { + if (null == this.taskList) { + throw new UnsupportedOperationException("Task info is not being kept!"); + } + return this.taskList.toArray(new TaskInfo[0]); + } + + /** + * 获取任务信息,类似于: + *
+	 *     StopWatch '[id]': running time = [total] ns
+	 * 
+ * + * @return 任务信息 + */ + public String shortSummary() { + return shortSummary(null); + } + + /** + * 获取任务信息,类似于: + *
+	 *     StopWatch '[id]': running time = [total] [unit]
+	 * 
+ * + * @param unit 时间单位,{@code null}则默认为{@link TimeUnit#NANOSECONDS} + * @return 任务信息 + */ + public String shortSummary(TimeUnit unit) { + if(null == unit){ + unit = TimeUnit.NANOSECONDS; + } + return StrUtil.format("StopWatch '{}': running time = {} {}", + this.id, getTotal(unit), DateUtil.getShotName(unit)); + } + + /** + * 生成所有任务的一个任务花费时间表,单位纳秒 + * + * @return 任务时间表 + */ + public String prettyPrint() { + return prettyPrint(null); + } + + /** + * 生成所有任务的一个任务花费时间表 + * + * @param unit 时间单位,{@code null}则默认{@link TimeUnit#NANOSECONDS} 纳秒 + * @return 任务时间表 + * @since 5.7.16 + */ + public String prettyPrint(TimeUnit unit) { + if (null == unit) { + unit = TimeUnit.NANOSECONDS; + } + + final StringBuilder sb = new StringBuilder(shortSummary(unit)); + sb.append(FileUtil.getLineSeparator()); + if (null == this.taskList) { + sb.append("No task info kept"); + } else { + sb.append("---------------------------------------------").append(FileUtil.getLineSeparator()); + sb.append(DateUtil.getShotName(unit)).append(" % Task name").append(FileUtil.getLineSeparator()); + sb.append("---------------------------------------------").append(FileUtil.getLineSeparator()); + + final NumberFormat nf = NumberFormat.getNumberInstance(); + nf.setMinimumIntegerDigits(9); + nf.setGroupingUsed(false); + + final NumberFormat pf = NumberFormat.getPercentInstance(); + pf.setMinimumIntegerDigits(2); + pf.setGroupingUsed(false); + + for (TaskInfo task : getTaskInfo()) { + sb.append(nf.format(task.getTime(unit))).append(" "); + sb.append(pf.format((double) task.getTimeNanos() / getTotalTimeNanos())).append(" "); + sb.append(task.getTaskName()).append(FileUtil.getLineSeparator()); + } + } + return sb.toString(); + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder(shortSummary()); + if (null != this.taskList) { + for (TaskInfo task : this.taskList) { + sb.append("; [").append(task.getTaskName()).append("] took ").append(task.getTimeNanos()).append(" ns"); + long percent = Math.round(100.0 * task.getTimeNanos() / getTotalTimeNanos()); + sb.append(" = ").append(percent).append("%"); + } + } else { + sb.append("; no task info kept"); + } + return sb.toString(); + } + + /** + * 存放任务名称和花费时间对象 + * + * @author Looly + */ + public static final class TaskInfo { + + private final String taskName; + private final long timeNanos; + + /** + * 构造 + * + * @param taskName 任务名称 + * @param timeNanos 花费时间(纳秒) + */ + TaskInfo(String taskName, long timeNanos) { + this.taskName = taskName; + this.timeNanos = timeNanos; + } + + /** + * 获取任务名 + * + * @return 任务名 + */ + public String getTaskName() { + return this.taskName; + } + + /** + * 获取指定单位的任务花费时间 + * + * @param unit 单位 + * @return 任务花费时间 + * @since 5.7.16 + */ + public long getTime(TimeUnit unit) { + return unit.convert(this.timeNanos, TimeUnit.NANOSECONDS); + } + + /** + * 获取任务花费时间(单位:纳秒) + * + * @return 任务花费时间(单位:纳秒) + * @see #getTimeMillis() + * @see #getTimeSeconds() + */ + public long getTimeNanos() { + return this.timeNanos; + } + + /** + * 获取任务花费时间(单位:毫秒) + * + * @return 任务花费时间(单位:毫秒) + * @see #getTimeNanos() + * @see #getTimeSeconds() + */ + public long getTimeMillis() { + return getTime(TimeUnit.MILLISECONDS); + } + + /** + * 获取任务花费时间(单位:秒) + * + * @return 任务花费时间(单位:秒) + * @see #getTimeMillis() + * @see #getTimeNanos() + */ + public double getTimeSeconds() { + return DateUtil.nanosToSeconds(this.timeNanos); + } + } +} diff --git a/src/main/java/cn/hutool/core/date/SystemClock.java b/src/main/java/cn/hutool/core/date/SystemClock.java new file mode 100644 index 0000000..6346dd2 --- /dev/null +++ b/src/main/java/cn/hutool/core/date/SystemClock.java @@ -0,0 +1,70 @@ +package cn.hutool.core.date; + +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +/** + * 系统时钟
+ * 高并发场景下System.currentTimeMillis()的性能问题的优化 + * System.currentTimeMillis()的调用比new一个普通对象要耗时的多(具体耗时高出多少我还没测试过,有人说是100倍左右) + * System.currentTimeMillis()之所以慢是因为去跟系统打了一次交道 + * 后台定时更新时钟,JVM退出时,线程自动回收 + * + * see: http://git.oschina.net/yu120/sequence + * @author lry,looly + */ +public class SystemClock { + + /** 时钟更新间隔,单位毫秒 */ + private final long period; + /** 现在时刻的毫秒数 */ + private volatile long now; + + /** + * 构造 + * @param period 时钟更新间隔,单位毫秒 + */ + public SystemClock(long period) { + this.period = period; + this.now = System.currentTimeMillis(); + scheduleClockUpdating(); + } + + /** + * 开启计时器线程 + */ + private void scheduleClockUpdating() { + ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(runnable -> { + Thread thread = new Thread(runnable, "System Clock"); + thread.setDaemon(true); + return thread; + }); + scheduler.scheduleAtFixedRate(() -> now = System.currentTimeMillis(), period, period, TimeUnit.MILLISECONDS); + } + + /** + * @return 当前时间毫秒数 + */ + private long currentTimeMillis() { + return now; + } + + //------------------------------------------------------------------------ static + /** + * 单例 + * @author Looly + * + */ + private static class InstanceHolder { + public static final SystemClock INSTANCE = new SystemClock(1); + } + + /** + * @return 当前时间 + */ + public static long now() { + return InstanceHolder.INSTANCE.currentTimeMillis(); + } + +} diff --git a/src/main/java/cn/hutool/core/date/TemporalAccessorUtil.java b/src/main/java/cn/hutool/core/date/TemporalAccessorUtil.java new file mode 100644 index 0000000..8517c74 --- /dev/null +++ b/src/main/java/cn/hutool/core/date/TemporalAccessorUtil.java @@ -0,0 +1,228 @@ +package cn.hutool.core.date; + +import cn.hutool.core.date.format.GlobalCustomFormat; +import cn.hutool.core.util.StrUtil; + +import java.time.DayOfWeek; +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.Month; +import java.time.MonthDay; +import java.time.OffsetDateTime; +import java.time.OffsetTime; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.time.chrono.Era; +import java.time.format.DateTimeFormatter; +import java.time.temporal.TemporalAccessor; +import java.time.temporal.TemporalField; +import java.time.temporal.UnsupportedTemporalTypeException; + +/** + * {@link TemporalAccessor} 工具类封装 + * + * @author looly + * @since 5.3.9 + */ +public class TemporalAccessorUtil extends TemporalUtil{ + + /** + * 安全获取时间的某个属性,属性不存在返回最小值,一般为0
+ * 注意请谨慎使用此方法,某些{@link TemporalAccessor#isSupported(TemporalField)}为{@code false}的方法返回最小值 + * + * @param temporalAccessor 需要获取的时间对象 + * @param field 需要获取的属性 + * @return 时间的值,如果无法获取则获取最小值,一般为0 + */ + public static int get(TemporalAccessor temporalAccessor, TemporalField field) { + if (temporalAccessor.isSupported(field)) { + return temporalAccessor.get(field); + } + + return (int)field.range().getMinimum(); + } + + /** + * 格式化日期时间为指定格式
+ * 如果为{@link Month},调用{@link Month#toString()} + * + * @param time {@link TemporalAccessor} + * @param formatter 日期格式化器,预定义的格式见:{@link DateTimeFormatter} + * @return 格式化后的字符串 + * @since 5.3.10 + */ + public static String format(TemporalAccessor time, DateTimeFormatter formatter) { + if (null == time) { + return null; + } + + if(time instanceof Month){ + return time.toString(); + } + + if(null == formatter){ + formatter = DateTimeFormatter.ISO_LOCAL_DATE_TIME; + } + + try { + return formatter.format(time); + } catch (UnsupportedTemporalTypeException e){ + if(time instanceof LocalDate && e.getMessage().contains("HourOfDay")){ + // 用户传入LocalDate,但是要求格式化带有时间部分,转换为LocalDateTime重试 + return formatter.format(((LocalDate) time).atStartOfDay()); + }else if(time instanceof LocalTime && e.getMessage().contains("YearOfEra")){ + // 用户传入LocalTime,但是要求格式化带有日期部分,转换为LocalDateTime重试 + return formatter.format(((LocalTime) time).atDate(LocalDate.now())); + } else if(time instanceof Instant){ + // 时间戳没有时区信息,赋予默认时区 + return formatter.format(((Instant) time).atZone(ZoneId.systemDefault())); + } + throw e; + } + } + + /** + * 格式化日期时间为指定格式
+ * 如果为{@link Month},调用{@link Month#toString()} + * + * @param time {@link TemporalAccessor} + * @param format 日期格式 + * @return 格式化后的字符串 + * @since 5.3.10 + */ + public static String format(TemporalAccessor time, String format) { + if (null == time) { + return null; + } + + if(time instanceof DayOfWeek || time instanceof Month || time instanceof Era || time instanceof MonthDay){ + return time.toString(); + } + + // 检查自定义格式 + if(GlobalCustomFormat.isCustomFormat(format)){ + return GlobalCustomFormat.format(time, format); + } + + final DateTimeFormatter formatter = StrUtil.isBlank(format) + ? null : DateTimeFormatter.ofPattern(format); + + return format(time, formatter); + } + + /** + * {@link TemporalAccessor}转换为 时间戳(从1970-01-01T00:00:00Z开始的毫秒数)
+ * 如果为{@link Month},调用{@link Month#getValue()} + * + * @param temporalAccessor Date对象 + * @return {@link Instant}对象 + * @since 5.4.1 + */ + public static long toEpochMilli(TemporalAccessor temporalAccessor) { + if(temporalAccessor instanceof Month){ + return ((Month) temporalAccessor).getValue(); + } else if(temporalAccessor instanceof DayOfWeek){ + return ((DayOfWeek) temporalAccessor).getValue(); + } else if(temporalAccessor instanceof Era){ + return ((Era) temporalAccessor).getValue(); + } + return toInstant(temporalAccessor).toEpochMilli(); + } + + /** + * {@link TemporalAccessor}转换为 {@link Instant}对象 + * + * @param temporalAccessor Date对象 + * @return {@link Instant}对象 + * @since 5.3.10 + */ + public static Instant toInstant(TemporalAccessor temporalAccessor) { + if (null == temporalAccessor) { + return null; + } + + Instant result; + if (temporalAccessor instanceof Instant) { + result = (Instant) temporalAccessor; + } else if (temporalAccessor instanceof LocalDateTime) { + result = ((LocalDateTime) temporalAccessor).atZone(ZoneId.systemDefault()).toInstant(); + } else if (temporalAccessor instanceof ZonedDateTime) { + result = ((ZonedDateTime) temporalAccessor).toInstant(); + } else if (temporalAccessor instanceof OffsetDateTime) { + result = ((OffsetDateTime) temporalAccessor).toInstant(); + } else if (temporalAccessor instanceof LocalDate) { + result = ((LocalDate) temporalAccessor).atStartOfDay(ZoneId.systemDefault()).toInstant(); + } else if (temporalAccessor instanceof LocalTime) { + // 指定本地时间转换 为Instant,取当天日期 + result = ((LocalTime) temporalAccessor).atDate(LocalDate.now()).atZone(ZoneId.systemDefault()).toInstant(); + } else if (temporalAccessor instanceof OffsetTime) { + // 指定本地时间转换 为Instant,取当天日期 + result = ((OffsetTime) temporalAccessor).atDate(LocalDate.now()).toInstant(); + } else { + // issue#1891@Github + // Instant.from不能完成日期转换 + //result = Instant.from(temporalAccessor); + result = toInstant(LocalDateTimeUtil.of(temporalAccessor)); + } + + return result; + } + + /** + * 当前日期是否在日期指定范围内
+ * 起始日期和结束日期可以互换 + * + * @param date 被检查的日期 + * @param beginDate 起始日期(包含) + * @param endDate 结束日期(包含) + * @return 是否在范围内 + * @since 5.8.5 + */ + public static boolean isIn(TemporalAccessor date, TemporalAccessor beginDate, TemporalAccessor endDate) { + return isIn(date, beginDate, endDate, true, true); + } + + /** + * 当前日期是否在日期指定范围内
+ * 起始日期和结束日期可以互换
+ * 通过includeBegin, includeEnd参数控制日期范围区间是否为开区间,例如:传入参数:includeBegin=true, includeEnd=false, + * 则本方法会判断 date ∈ (beginDate, endDate] 是否成立 + * + * @param date 被检查的日期 + * @param beginDate 起始日期 + * @param endDate 结束日期 + * @param includeBegin 时间范围是否包含起始日期 + * @param includeEnd 时间范围是否包含结束日期 + * @return 是否在范围内 + * @author FengBaoheng + * @since 5.8.6 + */ + public static boolean isIn(TemporalAccessor date, TemporalAccessor beginDate, TemporalAccessor endDate, + boolean includeBegin, boolean includeEnd) { + if (date == null || beginDate == null || endDate == null) { + throw new IllegalArgumentException("参数不可为null"); + } + + final long thisMills = toEpochMilli(date); + final long beginMills = toEpochMilli(beginDate); + final long endMills = toEpochMilli(endDate); + final long rangeMin = Math.min(beginMills, endMills); + final long rangeMax = Math.max(beginMills, endMills); + + // 先判断是否满足 date ∈ (beginDate, endDate) + boolean isIn = rangeMin < thisMills && thisMills < rangeMax; + + // 若不满足,则再判断是否在时间范围的边界上 + if (!isIn && includeBegin) { + isIn = thisMills == rangeMin; + } + + if (!isIn && includeEnd) { + isIn = thisMills == rangeMax; + } + + return isIn; + } +} diff --git a/src/main/java/cn/hutool/core/date/TemporalUtil.java b/src/main/java/cn/hutool/core/date/TemporalUtil.java new file mode 100644 index 0000000..6bf7d48 --- /dev/null +++ b/src/main/java/cn/hutool/core/date/TemporalUtil.java @@ -0,0 +1,141 @@ +package cn.hutool.core.date; + +import java.time.DayOfWeek; +import java.time.Duration; +import java.time.temporal.ChronoUnit; +import java.time.temporal.Temporal; +import java.time.temporal.TemporalAdjusters; +import java.time.temporal.TemporalUnit; +import java.util.concurrent.TimeUnit; + +/** + * {@link Temporal} 工具类封装 + * + * @author looly + * @since 5.4.5 + */ +public class TemporalUtil { + + /** + * 获取两个日期的差,如果结束时间早于开始时间,获取结果为负。 + *

+ * 返回结果为{@link Duration}对象,通过调用toXXX方法返回相差单位 + * + * @param startTimeInclude 开始时间(包含) + * @param endTimeExclude 结束时间(不包含) + * @return 时间差 {@link Duration}对象 + */ + public static Duration between(Temporal startTimeInclude, Temporal endTimeExclude) { + return Duration.between(startTimeInclude, endTimeExclude); + } + + /** + * 获取两个日期的差,如果结束时间早于开始时间,获取结果为负。 + *

+ * 返回结果为时间差的long值 + * + * @param startTimeInclude 开始时间(包括) + * @param endTimeExclude 结束时间(不包括) + * @param unit 时间差单位 + * @return 时间差 + */ + public static long between(Temporal startTimeInclude, Temporal endTimeExclude, ChronoUnit unit) { + return unit.between(startTimeInclude, endTimeExclude); + } + + /** + * 将 {@link TimeUnit} 转换为 {@link ChronoUnit}. + * + * @param unit 被转换的{@link TimeUnit}单位,如果为{@code null}返回{@code null} + * @return {@link ChronoUnit} + * @since 5.7.16 + */ + public static ChronoUnit toChronoUnit(TimeUnit unit) throws IllegalArgumentException { + if (null == unit) { + return null; + } + switch (unit) { + case NANOSECONDS: + return ChronoUnit.NANOS; + case MICROSECONDS: + return ChronoUnit.MICROS; + case MILLISECONDS: + return ChronoUnit.MILLIS; + case SECONDS: + return ChronoUnit.SECONDS; + case MINUTES: + return ChronoUnit.MINUTES; + case HOURS: + return ChronoUnit.HOURS; + case DAYS: + return ChronoUnit.DAYS; + default: + throw new IllegalArgumentException("Unknown TimeUnit constant"); + } + } + + /** + * 转换 {@link ChronoUnit} 到 {@link TimeUnit}. + * + * @param unit {@link ChronoUnit},如果为{@code null}返回{@code null} + * @return {@link TimeUnit} + * @throws IllegalArgumentException 如果{@link TimeUnit}没有对应单位抛出 + * @since 5.7.16 + */ + public static TimeUnit toTimeUnit(ChronoUnit unit) throws IllegalArgumentException { + if (null == unit) { + return null; + } + switch (unit) { + case NANOS: + return TimeUnit.NANOSECONDS; + case MICROS: + return TimeUnit.MICROSECONDS; + case MILLIS: + return TimeUnit.MILLISECONDS; + case SECONDS: + return TimeUnit.SECONDS; + case MINUTES: + return TimeUnit.MINUTES; + case HOURS: + return TimeUnit.HOURS; + case DAYS: + return TimeUnit.DAYS; + default: + throw new IllegalArgumentException("ChronoUnit cannot be converted to TimeUnit: " + unit); + } + } + + /** + * 日期偏移,根据field不同加不同值(偏移会修改传入的对象) + * + * @param 日期类型,如LocalDate或LocalDateTime + * @param time {@link Temporal} + * @param number 偏移量,正数为向后偏移,负数为向前偏移 + * @param field 偏移单位,见{@link ChronoUnit},不能为null + * @return 偏移后的日期时间 + */ + @SuppressWarnings("unchecked") + public static T offset(T time, long number, TemporalUnit field) { + if (null == time) { + return null; + } + + return (T) time.plus(number, field); + } + + /** + * 偏移到指定的周几 + * + * @param temporal 日期或者日期时间 + * @param dayOfWeek 周几 + * @param 日期类型,如LocalDate或LocalDateTime + * @param isPrevious 是否向前偏移,{@code true}向前偏移,{@code false}向后偏移。 + * @return 偏移后的日期 + * @since 5.8.0 + */ + @SuppressWarnings("unchecked") + public T offset(T temporal, DayOfWeek dayOfWeek, boolean isPrevious) { + return (T) temporal.with(isPrevious ? TemporalAdjusters.previous(dayOfWeek) : TemporalAdjusters.next(dayOfWeek)); + } +} diff --git a/src/main/java/cn/hutool/core/date/TimeInterval.java b/src/main/java/cn/hutool/core/date/TimeInterval.java new file mode 100644 index 0000000..e0c7d8a --- /dev/null +++ b/src/main/java/cn/hutool/core/date/TimeInterval.java @@ -0,0 +1,133 @@ +package cn.hutool.core.date; + +import cn.hutool.core.util.StrUtil; + +/** + * 计时器
+ * 计算某个过程花费的时间,精确到毫秒或纳秒 + * + * @author Looly + */ +public class TimeInterval extends GroupTimeInterval { + private static final long serialVersionUID = 1L; + private static final String DEFAULT_ID = StrUtil.EMPTY; + + /** + * 构造,默认使用毫秒计数 + */ + public TimeInterval() { + this(false); + } + + /** + * 构造 + * + * @param isNano 是否使用纳秒计数,false则使用毫秒 + */ + public TimeInterval(boolean isNano) { + super(isNano); + start(); + } + + /** + * @return 开始计时并返回当前时间 + */ + public long start() { + return start(DEFAULT_ID); + } + + /** + * @return 重新计时并返回从开始到当前的持续时间 + */ + public long intervalRestart() { + return intervalRestart(DEFAULT_ID); + } + + /** + * 重新开始计算时间(重置开始时间) + * + * @return this + * @see #start() + * @since 3.0.1 + */ + public TimeInterval restart() { + start(DEFAULT_ID); + return this; + } + + //----------------------------------------------------------- Interval + + /** + * 从开始到当前的间隔时间(毫秒数)
+ * 如果使用纳秒计时,返回纳秒差,否则返回毫秒差 + * + * @return 从开始到当前的间隔时间(毫秒数) + */ + public long interval() { + return interval(DEFAULT_ID); + } + + /** + * 从开始到当前的间隔时间(毫秒数),返回XX天XX小时XX分XX秒XX毫秒 + * + * @return 从开始到当前的间隔时间(毫秒数) + * @since 4.6.7 + */ + public String intervalPretty() { + return intervalPretty(DEFAULT_ID); + } + + /** + * 从开始到当前的间隔时间(毫秒数) + * + * @return 从开始到当前的间隔时间(毫秒数) + */ + public long intervalMs() { + return intervalMs(DEFAULT_ID); + } + + /** + * 从开始到当前的间隔秒数,取绝对值 + * + * @return 从开始到当前的间隔秒数,取绝对值 + */ + public long intervalSecond() { + return intervalSecond(DEFAULT_ID); + } + + /** + * 从开始到当前的间隔分钟数,取绝对值 + * + * @return 从开始到当前的间隔分钟数,取绝对值 + */ + public long intervalMinute() { + return intervalMinute(DEFAULT_ID); + } + + /** + * 从开始到当前的间隔小时数,取绝对值 + * + * @return 从开始到当前的间隔小时数,取绝对值 + */ + public long intervalHour() { + return intervalHour(DEFAULT_ID); + } + + /** + * 从开始到当前的间隔天数,取绝对值 + * + * @return 从开始到当前的间隔天数,取绝对值 + */ + public long intervalDay() { + return intervalDay(DEFAULT_ID); + } + + /** + * 从开始到当前的间隔周数,取绝对值 + * + * @return 从开始到当前的间隔周数,取绝对值 + */ + public long intervalWeek() { + return intervalWeek(DEFAULT_ID); + } +} diff --git a/src/main/java/cn/hutool/core/date/Week.java b/src/main/java/cn/hutool/core/date/Week.java new file mode 100644 index 0000000..396a80d --- /dev/null +++ b/src/main/java/cn/hutool/core/date/Week.java @@ -0,0 +1,205 @@ +package cn.hutool.core.date; + +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.ArrayUtil; + +import java.time.DayOfWeek; +import java.util.Calendar; + +/** + * 星期枚举
+ * 与Calendar中的星期int值对应 + * + * @author Looly + * @see #SUNDAY + * @see #MONDAY + * @see #TUESDAY + * @see #WEDNESDAY + * @see #THURSDAY + * @see #FRIDAY + * @see #SATURDAY + */ +public enum Week { + + /** + * 周日 + */ + SUNDAY(Calendar.SUNDAY), + /** + * 周一 + */ + MONDAY(Calendar.MONDAY), + /** + * 周二 + */ + TUESDAY(Calendar.TUESDAY), + /** + * 周三 + */ + WEDNESDAY(Calendar.WEDNESDAY), + /** + * 周四 + */ + THURSDAY(Calendar.THURSDAY), + /** + * 周五 + */ + FRIDAY(Calendar.FRIDAY), + /** + * 周六 + */ + SATURDAY(Calendar.SATURDAY); + + // --------------------------------------------------------------- + /** + * Weeks aliases. + */ + private static final String[] ALIASES = {"sun", "mon", "tue", "wed", "thu", "fri", "sat"}; + private static final Week[] ENUMS = Week.values(); + + /** + * 星期对应{@link Calendar} 中的Week值 + */ + private final int value; + + /** + * 构造 + * + * @param value 星期对应{@link Calendar} 中的Week值 + */ + Week(int value) { + this.value = value; + } + + /** + * 获得星期对应{@link Calendar} 中的Week值 + * + * @return 星期对应 {@link Calendar} 中的Week值 + */ + public int getValue() { + return this.value; + } + + /** + * 获取ISO8601规范的int值,from 1 (Monday) to 7 (Sunday). + * + * @return ISO8601规范的int值 + * @since 5.8.0 + */ + public int getIso8601Value(){ + int iso8601IntValue = getValue() -1; + if(0 == iso8601IntValue){ + iso8601IntValue = 7; + } + return iso8601IntValue; + } + + /** + * 转换为中文名 + * + * @return 星期的中文名 + * @since 3.3.0 + */ + public String toChinese() { + return toChinese("星期"); + } + + /** + * 转换为中文名 + * + * @param weekNamePre 表示星期的前缀,例如前缀为“星期”,则返回结果为“星期一”;前缀为”周“,结果为“周一” + * @return 星期的中文名 + * @since 4.0.11 + */ + public String toChinese(String weekNamePre) { + switch (this) { + case SUNDAY: + return weekNamePre + "日"; + case MONDAY: + return weekNamePre + "一"; + case TUESDAY: + return weekNamePre + "二"; + case WEDNESDAY: + return weekNamePre + "三"; + case THURSDAY: + return weekNamePre + "四"; + case FRIDAY: + return weekNamePre + "五"; + case SATURDAY: + return weekNamePre + "六"; + default: + return null; + } + } + + /** + * 转换为{@link DayOfWeek} + * + * @return {@link DayOfWeek} + * @since 5.8.0 + */ + public DayOfWeek toJdkDayOfWeek() { + return DayOfWeek.of(getIso8601Value()); + } + + /** + * 将 {@link Calendar}星期相关值转换为Week枚举对象
+ * + * @param calendarWeekIntValue Calendar中关于Week的int值,1表示Sunday + * @return Week + * @see #SUNDAY + * @see #MONDAY + * @see #TUESDAY + * @see #WEDNESDAY + * @see #THURSDAY + * @see #FRIDAY + * @see #SATURDAY + */ + public static Week of(int calendarWeekIntValue) { + if (calendarWeekIntValue > ENUMS.length || calendarWeekIntValue < 1) { + return null; + } + return ENUMS[calendarWeekIntValue - 1]; + } + + /** + * 解析别名为Week对象,别名如:sun或者SUNDAY,不区分大小写 + * + * @param name 别名值 + * @return 周枚举Week,非空 + * @throws IllegalArgumentException 如果别名无对应的枚举,抛出此异常 + * @since 5.8.0 + */ + public static Week of(String name) throws IllegalArgumentException { + Assert.notBlank(name); + Week of = of(ArrayUtil.indexOfIgnoreCase(ALIASES, name) + 1); + if (null == of) { + of = Week.valueOf(name.toUpperCase()); + } + return of; + } + + /** + * 将 {@link DayOfWeek}星期相关值转换为Week枚举对象
+ * + * @param dayOfWeek DayOfWeek星期值 + * @return Week + * @see #SUNDAY + * @see #MONDAY + * @see #TUESDAY + * @see #WEDNESDAY + * @see #THURSDAY + * @see #FRIDAY + * @see #SATURDAY + * @since 5.7.14 + */ + public static Week of(DayOfWeek dayOfWeek) { + Assert.notNull(dayOfWeek); + int week = dayOfWeek.getValue() + 1; + if(8 == week){ + // 周日 + week = 1; + } + return of(week); + } +} diff --git a/src/main/java/cn/hutool/core/date/Zodiac.java b/src/main/java/cn/hutool/core/date/Zodiac.java new file mode 100644 index 0000000..9e504f4 --- /dev/null +++ b/src/main/java/cn/hutool/core/date/Zodiac.java @@ -0,0 +1,103 @@ +package cn.hutool.core.date; + +import java.util.Calendar; +import java.util.Date; + +/** + * 星座 来自:https://blog.csdn.net/u010758605/article/details/48317881 + * + * @author looly + * @since 4.4.3 + */ +public class Zodiac { + + /** 星座分隔时间日 */ + private static final int[] DAY_ARR = new int[] { 20, 19, 21, 20, 21, 22, 23, 23, 23, 24, 23, 22 }; + /** 星座 */ + private static final String[] ZODIACS = new String[] { "摩羯座", "水瓶座", "双鱼座", "白羊座", "金牛座", "双子座", "巨蟹座", "狮子座", "处女座", "天秤座", "天蝎座", "射手座", "摩羯座" }; + private static final String[] CHINESE_ZODIACS = new String[] { "鼠", "牛", "虎", "兔", "龙", "蛇", "马", "羊", "猴", "鸡", "狗", "猪" }; + + /** + * 通过生日计算星座 + * + * @param date 出生日期 + * @return 星座名 + */ + public static String getZodiac(Date date) { + return getZodiac(DateUtil.calendar(date)); + } + + /** + * 通过生日计算星座 + * + * @param calendar 出生日期 + * @return 星座名 + */ + public static String getZodiac(Calendar calendar) { + if (null == calendar) { + return null; + } + return getZodiac(calendar.get(Calendar.MONTH), calendar.get(Calendar.DAY_OF_MONTH)); + } + + /** + * 通过生日计算星座 + * + * @param month 月,从0开始计数 + * @param day 天 + * @return 星座名 + * @since 4.5.0 + */ + public static String getZodiac(Month month, int day) { + return getZodiac(month.getValue(), day); + } + + /** + * 通过生日计算星座 + * + * @param month 月,从0开始计数,见{@link Month#getValue()} + * @param day 天 + * @return 星座名 + */ + public static String getZodiac(int month, int day) { + // 在分隔日前为前一个星座,否则为后一个星座 + return day < DAY_ARR[month] ? ZODIACS[month] : ZODIACS[month + 1]; + } + + // ----------------------------------------------------------------------------------------------------------- 生肖 + /** + * 通过生日计算生肖,只计算1900年后出生的人 + * + * @param date 出生日期(年需农历) + * @return 星座名 + */ + public static String getChineseZodiac(Date date) { + return getChineseZodiac(DateUtil.calendar(date)); + } + + /** + * 通过生日计算生肖,只计算1900年后出生的人 + * + * @param calendar 出生日期(年需农历) + * @return 星座名 + */ + public static String getChineseZodiac(Calendar calendar) { + if (null == calendar) { + return null; + } + return getChineseZodiac(calendar.get(Calendar.YEAR)); + } + + /** + * 计算生肖,只计算1900年后出生的人 + * + * @param year 农历年 + * @return 生肖名 + */ + public static String getChineseZodiac(int year) { + if (year < 1900) { + return null; + } + return CHINESE_ZODIACS[(year - 1900) % CHINESE_ZODIACS.length]; + } +} diff --git a/src/main/java/cn/hutool/core/date/ZoneUtil.java b/src/main/java/cn/hutool/core/date/ZoneUtil.java new file mode 100644 index 0000000..1b12788 --- /dev/null +++ b/src/main/java/cn/hutool/core/date/ZoneUtil.java @@ -0,0 +1,41 @@ +package cn.hutool.core.date; + +import java.time.ZoneId; +import java.util.TimeZone; + +/** + * {@link ZoneId}和{@link TimeZone}相关封装 + * + * @author looly + * @since 5.7.15 + */ +public class ZoneUtil { + + /** + * {@link ZoneId}转换为{@link TimeZone},{@code null}则返回系统默认值 + * + * @param zoneId {@link ZoneId},{@code null}则返回系统默认值 + * @return {@link TimeZone} + */ + public static TimeZone toTimeZone(ZoneId zoneId) { + if (null == zoneId) { + return TimeZone.getDefault(); + } + + return TimeZone.getTimeZone(zoneId); + } + + /** + * {@link TimeZone}转换为{@link ZoneId},{@code null}则返回系统默认值 + * + * @param timeZone {@link TimeZone},{@code null}则返回系统默认值 + * @return {@link ZoneId} + */ + public static ZoneId toZoneId(TimeZone timeZone) { + if (null == timeZone) { + return ZoneId.systemDefault(); + } + + return timeZone.toZoneId(); + } +} diff --git a/src/main/java/cn/hutool/core/date/chinese/ChineseMonth.java b/src/main/java/cn/hutool/core/date/chinese/ChineseMonth.java new file mode 100644 index 0000000..3dabe0b --- /dev/null +++ b/src/main/java/cn/hutool/core/date/chinese/ChineseMonth.java @@ -0,0 +1,39 @@ +package cn.hutool.core.date.chinese; + +/** + * 农历月份表示 + * + * @author looly + * @since 5.4.1 + */ +public class ChineseMonth { + + private static final String[] MONTH_NAME = {"一", "二", "三", "四", "五", "六", "七", "八", "九", "十", "十一", "十二"}; + private static final String[] MONTH_NAME_TRADITIONAL = {"正", "二", "三", "四", "五", "六", "七", "八", "九", "寒", "冬", "腊"}; + + /** + * 当前农历月份是否为闰月 + * + * @param year 农历年 + * @param month 农历月 + * @return 是否为闰月 + * @since 5.4.2 + */ + public static boolean isLeapMonth(int year, int month) { + return month == LunarInfo.leapMonth(year); + } + + /** + * 获得农历月称呼
+ * 当为传统表示时,表示为二月,腊月,或者润正月等 + * 当为非传统表示时,二月,十二月,或者润一月等 + * + * @param isLeapMonth 是否闰月 + * @param month 月份,从1开始,如果是闰月,应传入需要显示的月份 + * @param isTraditional 是否传统表示,例如一月传统表示为正月 + * @return 返回农历月份称呼 + */ + public static String getChineseMonthName(boolean isLeapMonth, int month, boolean isTraditional) { + return (isLeapMonth ? "闰" : "") + (isTraditional ? MONTH_NAME_TRADITIONAL : MONTH_NAME)[month - 1] + "月"; + } +} diff --git a/src/main/java/cn/hutool/core/date/chinese/GanZhi.java b/src/main/java/cn/hutool/core/date/chinese/GanZhi.java new file mode 100644 index 0000000..f56ccab --- /dev/null +++ b/src/main/java/cn/hutool/core/date/chinese/GanZhi.java @@ -0,0 +1,81 @@ +package cn.hutool.core.date.chinese; + +import java.time.LocalDate; + +/** + * 天干地支类 + * 天干地支,简称为干支 + * + * @author looly + * @since 5.4.1 + */ +public class GanZhi { + + /** + * 十天干:甲(jiǎ)、乙(yǐ)、丙(bǐng)、丁(dīng)、戊(wù)、己(jǐ)、庚(gēng)、辛(xīn)、壬(rén)、癸(guǐ) + * 十二地支:子(zǐ)、丑(chǒu)、寅(yín)、卯(mǎo)、辰(chén)、巳(sì)、午(wǔ)、未(wèi)、申(shēn)、酉(yǒu)、戌(xū)、亥(hài) + * 十二地支对应十二生肖:子-鼠,丑-牛,寅-虎,卯-兔,辰-龙,巳-蛇, 午-马,未-羊,申-猴,酉-鸡,戌-狗,亥-猪 + * + * @see 天干地支:简称,干支 + */ + private static final String[] GAN = new String[]{"甲", "乙", "丙", "丁", "戊", "己", "庚", "辛", "壬", "癸"}; + private static final String[] ZHI = new String[]{"子", "丑", "寅", "卯", "辰", "巳", "午", "未", "申", "酉", "戌", "亥"}; + + /** + * 传入 月日的offset 传回干支, 0=甲子 + * + * @param num 月日的offset + * @return 干支 + */ + public static String cyclicalm(int num) { + return (GAN[num % 10] + ZHI[num % 12]); + } + + /** + * 传入年传回干支 + * + * @param year 农历年 + * @return 干支 + * @since 5.4.7 + */ + public static String getGanzhiOfYear(int year) { + // 1864年(1900 - 36)是甲子年,用于计算基准的干支年 + return cyclicalm(year - LunarInfo.BASE_YEAR + 36); + } + + /** + * 获取干支月 + * + * @param year 公历年 + * @param month 公历月,从1开始 + * @param day 公历日 + * @return 干支月 + * @since 5.4.7 + */ + public static String getGanzhiOfMonth(int year, int month, int day) { + //返回当月「节」为几日开始 + int firstNode = SolarTerms.getTerm(year, (month * 2 - 1)); + // 依据12节气修正干支月 + int monthOffset = (year - LunarInfo.BASE_YEAR) * 12 + month + 11; + if (day >= firstNode) { + monthOffset++; + } + return cyclicalm(monthOffset); + } + + /** + * 获取干支日 + * + * @param year 公历年 + * @param month 公历月,从1开始 + * @param day 公历日 + * @return 干支 + * @since 5.4.7 + */ + public static String getGanzhiOfDay(int year, int month, int day) { + // 与1970-01-01相差天数,不包括当天 + final long days = LocalDate.of(year, month, day).toEpochDay() - 1; + //1899-12-21是农历1899年腊月甲子日 41:相差1900-01-31有41天 + return cyclicalm((int) (days - LunarInfo.BASE_DAY + 41)); + } +} diff --git a/src/main/java/cn/hutool/core/date/chinese/LunarFestival.java b/src/main/java/cn/hutool/core/date/chinese/LunarFestival.java new file mode 100644 index 0000000..20e9e55 --- /dev/null +++ b/src/main/java/cn/hutool/core/date/chinese/LunarFestival.java @@ -0,0 +1,115 @@ +package cn.hutool.core.date.chinese; + +import cn.hutool.core.lang.Pair; +import cn.hutool.core.map.TableMap; + +import java.util.List; + +/** + * 节假日(农历)封装 + * + * @author looly + * @since 5.4.1 + */ +public class LunarFestival { + + //农历节日 *表示放假日 + // 来自:https://baike.baidu.com/item/%E4%B8%AD%E5%9B%BD%E4%BC%A0%E7%BB%9F%E8%8A%82%E6%97%A5/396100 + private static final TableMap, String> L_FTV = new TableMap<>(16); + + static { + // 节日 + L_FTV.put(new Pair<>(1, 1), "春节"); + L_FTV.put(new Pair<>(1, 2), "犬日"); + L_FTV.put(new Pair<>(1, 3), "猪日"); + L_FTV.put(new Pair<>(1, 4), "羊日"); + L_FTV.put(new Pair<>(1, 5), "牛日 破五日"); + L_FTV.put(new Pair<>(1, 6), "马日 送穷日"); + L_FTV.put(new Pair<>(1, 7), "人日 人胜节"); + L_FTV.put(new Pair<>(1, 8), "谷日 八仙日"); + L_FTV.put(new Pair<>(1, 9), "天日 九皇会"); + L_FTV.put(new Pair<>(1, 10), "地日 石头生日"); + L_FTV.put(new Pair<>(1, 12), "火日 老鼠娶媳妇日"); + L_FTV.put(new Pair<>(1, 13), "上(试)灯日 关公升天日"); + L_FTV.put(new Pair<>(1, 15), "元宵节 上元节"); + L_FTV.put(new Pair<>(1, 18), "落灯日"); + + // 二月 + L_FTV.put(new Pair<>(2, 1), "中和节 太阳生日"); + L_FTV.put(new Pair<>(2, 2), "龙抬头"); + L_FTV.put(new Pair<>(2, 12), "花朝节"); + L_FTV.put(new Pair<>(2, 19), "观世音圣诞"); + + // 三月 + L_FTV.put(new Pair<>(3, 3), "上巳节"); + + // 四月 + L_FTV.put(new Pair<>(4, 1), "祭雹神"); + L_FTV.put(new Pair<>(4, 4), "文殊菩萨诞辰"); + L_FTV.put(new Pair<>(4, 8), "佛诞节"); + + // 五月 + L_FTV.put(new Pair<>(5, 5), "端午节 端阳节"); + + // 六月 + L_FTV.put(new Pair<>(6, 6), "晒衣节 姑姑节"); + L_FTV.put(new Pair<>(6, 6), "天贶节"); + L_FTV.put(new Pair<>(6, 24), "彝族火把节"); + + // 七月 + L_FTV.put(new Pair<>(7, 7), "七夕"); + L_FTV.put(new Pair<>(7, 14), "鬼节(南方)"); + L_FTV.put(new Pair<>(7, 15), "中元节"); + L_FTV.put(new Pair<>(7, 15), "盂兰盆节 中元节"); + L_FTV.put(new Pair<>(7, 30), "地藏节"); + + // 八月 + L_FTV.put(new Pair<>(8, 15), "中秋节"); + + // 九月 + L_FTV.put(new Pair<>(9, 9), "重阳节"); + + // 十月 + L_FTV.put(new Pair<>(10, 1), "祭祖节"); + L_FTV.put(new Pair<>(10, 15), "下元节"); + + // 十一月 + L_FTV.put(new Pair<>(11, 17), "阿弥陀佛圣诞"); + + // 腊月 + L_FTV.put(new Pair<>(12, 8), "腊八节"); + L_FTV.put(new Pair<>(12, 16), "尾牙"); + L_FTV.put(new Pair<>(12, 23), "小年"); + L_FTV.put(new Pair<>(12, 30), "除夕"); + } + + /** + * 获得节日列表 + * + * @param year 年 + * @param month 月 + * @param day 日 + * @return 获得农历节日 + * @since 5.4.5 + */ + public static List getFestivals(int year, int month, int day) { + // 春节判断,如果12月是小月,则29为除夕,否则30为除夕 + if (12 == month && 29 == day) { + if (29 == LunarInfo.monthDays(year, month)) { + day++; + } + } + return getFestivals(month, day); + } + + /** + * 获得节日列表,此方法无法判断月是否为大月或小月 + * + * @param month 月 + * @param day 日 + * @return 获得农历节日 + */ + public static List getFestivals(int month, int day) { + return L_FTV.getValues(new Pair<>(month, day)); + } +} diff --git a/src/main/java/cn/hutool/core/date/chinese/LunarInfo.java b/src/main/java/cn/hutool/core/date/chinese/LunarInfo.java new file mode 100644 index 0000000..d8c479e --- /dev/null +++ b/src/main/java/cn/hutool/core/date/chinese/LunarInfo.java @@ -0,0 +1,116 @@ +package cn.hutool.core.date.chinese; + +import java.time.LocalDate; + +/** + * 阴历(农历)信息 + * + * @author looly + * @since 5.4.1 + */ +public class LunarInfo { + + /** + * 1900年 + */ + public static final int BASE_YEAR = 1900; + /** + * 1900-01-31,农历正月初一 + */ + public static final long BASE_DAY = LocalDate.of(BASE_YEAR, 1, 31).toEpochDay(); + + /** + * 此表来自:https://github.com/jjonline/calendar.js/blob/master/calendar.js + * 农历表示: + * 1. 表示当年有无闰年,有的话,为闰月的月份,没有的话,为0。 + * 2-4.为除了闰月外的正常月份是大月还是小月,1为30天,0为29天。 + * 5. 表示闰月是大月还是小月,仅当存在闰月的情况下有意义。 + */ + private static final long[] LUNAR_CODE = new long[]{ + 0x04bd8, 0x04ae0, 0x0a570, 0x054d5, 0x0d260, 0x0d950, 0x16554, 0x056a0, 0x09ad0, 0x055d2,//1900-1909 + 0x04ae0, 0x0a5b6, 0x0a4d0, 0x0d250, 0x1d255, 0x0b540, 0x0d6a0, 0x0ada2, 0x095b0, 0x14977,//1910-1919 + 0x04970, 0x0a4b0, 0x0b4b5, 0x06a50, 0x06d40, 0x1ab54, 0x02b60, 0x09570, 0x052f2, 0x04970,//1920-1929 + 0x06566, 0x0d4a0, 0x0ea50, 0x16a95, 0x05ad0, 0x02b60, 0x186e3, 0x092e0, 0x1c8d7, 0x0c950,//1930-1939 + 0x0d4a0, 0x1d8a6, 0x0b550, 0x056a0, 0x1a5b4, 0x025d0, 0x092d0, 0x0d2b2, 0x0a950, 0x0b557,//1940-1949 + 0x06ca0, 0x0b550, 0x15355, 0x04da0, 0x0a5b0, 0x14573, 0x052b0, 0x0a9a8, 0x0e950, 0x06aa0,//1950-1959 + 0x0aea6, 0x0ab50, 0x04b60, 0x0aae4, 0x0a570, 0x05260, 0x0f263, 0x0d950, 0x05b57, 0x056a0,//1960-1969 + 0x096d0, 0x04dd5, 0x04ad0, 0x0a4d0, 0x0d4d4, 0x0d250, 0x0d558, 0x0b540, 0x0b6a0, 0x195a6,//1970-1979 + 0x095b0, 0x049b0, 0x0a974, 0x0a4b0, 0x0b27a, 0x06a50, 0x06d40, 0x0af46, 0x0ab60, 0x09570,//1980-1989 + 0x04af5, 0x04970, 0x064b0, 0x074a3, 0x0ea50, 0x06b58, 0x05ac0, 0x0ab60, 0x096d5, 0x092e0,//1990-1999 + 0x0c960, 0x0d954, 0x0d4a0, 0x0da50, 0x07552, 0x056a0, 0x0abb7, 0x025d0, 0x092d0, 0x0cab5,//2000-2009 + 0x0a950, 0x0b4a0, 0x0baa4, 0x0ad50, 0x055d9, 0x04ba0, 0x0a5b0, 0x15176, 0x052b0, 0x0a930,//2010-2019 + 0x07954, 0x06aa0, 0x0ad50, 0x05b52, 0x04b60, 0x0a6e6, 0x0a4e0, 0x0d260, 0x0ea65, 0x0d530,//2020-2029 + 0x05aa0, 0x076a3, 0x096d0, 0x04afb, 0x04ad0, 0x0a4d0, 0x1d0b6, 0x0d250, 0x0d520, 0x0dd45,//2030-2039 + 0x0b5a0, 0x056d0, 0x055b2, 0x049b0, 0x0a577, 0x0a4b0, 0x0aa50, 0x1b255, 0x06d20, 0x0ada0,//2040-2049 + 0x14b63, 0x09370, 0x049f8, 0x04970, 0x064b0, 0x168a6, 0x0ea50, 0x06b20, 0x1a6c4, 0x0aae0,//2050-2059 + 0x092e0, 0x0d2e3, 0x0c960, 0x0d557, 0x0d4a0, 0x0da50, 0x05d55, 0x056a0, 0x0a6d0, 0x055d4,//2060-2069 + 0x052d0, 0x0a9b8, 0x0a950, 0x0b4a0, 0x0b6a6, 0x0ad50, 0x055a0, 0x0aba4, 0x0a5b0, 0x052b0,//2070-2079 + 0x0b273, 0x06930, 0x07337, 0x06aa0, 0x0ad50, 0x14b55, 0x04b60, 0x0a570, 0x054e4, 0x0d160,//2080-2089 + 0x0e968, 0x0d520, 0x0daa0, 0x16aa6, 0x056d0, 0x04ae0, 0x0a9d4, 0x0a2d0, 0x0d150, 0x0f252,//2090-2099 + }; + + // 支持的最大年限 + public static final int MAX_YEAR = BASE_YEAR + LUNAR_CODE.length - 1; + + /** + * 传回农历 y年的总天数 + * + * @param y 年 + * @return 总天数 + */ + public static int yearDays(int y) { + int i, sum = 348; + for (i = 0x8000; i > 0x8; i >>= 1) { + if ((getCode(y) & i) != 0) { + sum += 1; + } + } + return (sum + leapDays(y)); + } + + /** + * 传回农历 y年闰月的天数,如果本年无闰月,返回0,区分大小月 + * + * @param y 农历年 + * @return 闰月的天数 + */ + public static int leapDays(int y) { + if (leapMonth(y) != 0) { + return (getCode(y) & 0x10000) != 0 ? 30 : 29; + } + + return 0; + } + + /** + * 传回农历 y年m月的总天数,区分大小月 + * + * @param y 年 + * @param m 月 + * @return 总天数 + */ + public static int monthDays(int y, int m) { + return (getCode(y) & (0x10000 >> m)) == 0 ? 29 : 30; + } + + /** + * 传回农历 y年闰哪个月 1-12 , 没闰传回 0
+ * 此方法会返回润N月中的N,如二月、闰二月都返回2 + * + * @param y 年 + * @return 润的月, 没闰传回 0 + */ + public static int leapMonth(int y) { + return (int) (getCode(y) & 0xf); + } + + /** + * 获取对应年的农历信息 + * + * @param year 年 + * @return 农历信息 + */ + private static long getCode(int year) { + return LUNAR_CODE[year - BASE_YEAR]; + } +} diff --git a/src/main/java/cn/hutool/core/date/chinese/SolarTerms.java b/src/main/java/cn/hutool/core/date/chinese/SolarTerms.java new file mode 100644 index 0000000..6c327e7 --- /dev/null +++ b/src/main/java/cn/hutool/core/date/chinese/SolarTerms.java @@ -0,0 +1,209 @@ +package cn.hutool.core.date.chinese; + +import cn.hutool.core.date.ChineseDate; +import cn.hutool.core.date.DateTime; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.util.NumberUtil; +import cn.hutool.core.util.StrUtil; + +import java.time.LocalDate; +import java.util.Date; + +/** + * 24节气相关信息 + * + * @author looly, zak + * @since 5.4.1 + */ +public class SolarTerms { + + /** + * 1900-2100各年的24节气日期速查表 + * 此表来自:https://github.com/jjonline/calendar.js/blob/master/calendar.js + */ + private static final String[] S_TERM_INFO = new String[]{ + "9778397bd097c36b0b6fc9274c91aa", "97b6b97bd19801ec9210c965cc920e", "97bcf97c3598082c95f8c965cc920f", + "97bd0b06bdb0722c965ce1cfcc920f", "b027097bd097c36b0b6fc9274c91aa", "97b6b97bd19801ec9210c965cc920e", + "97bcf97c359801ec95f8c965cc920f", "97bd0b06bdb0722c965ce1cfcc920f", "b027097bd097c36b0b6fc9274c91aa", + "97b6b97bd19801ec9210c965cc920e", "97bcf97c359801ec95f8c965cc920f", "97bd0b06bdb0722c965ce1cfcc920f", + "b027097bd097c36b0b6fc9274c91aa", "9778397bd19801ec9210c965cc920e", "97b6b97bd19801ec95f8c965cc920f", + "97bd09801d98082c95f8e1cfcc920f", "97bd097bd097c36b0b6fc9210c8dc2", "9778397bd197c36c9210c9274c91aa", + "97b6b97bd19801ec95f8c965cc920e", "97bd09801d98082c95f8e1cfcc920f", "97bd097bd097c36b0b6fc9210c8dc2", + "9778397bd097c36c9210c9274c91aa", "97b6b97bd19801ec95f8c965cc920e", "97bcf97c3598082c95f8e1cfcc920f", + "97bd097bd097c36b0b6fc9210c8dc2", "9778397bd097c36c9210c9274c91aa", "97b6b97bd19801ec9210c965cc920e", + "97bcf97c3598082c95f8c965cc920f", "97bd097bd097c35b0b6fc920fb0722", "9778397bd097c36b0b6fc9274c91aa", + "97b6b97bd19801ec9210c965cc920e", "97bcf97c3598082c95f8c965cc920f", "97bd097bd097c35b0b6fc920fb0722", + "9778397bd097c36b0b6fc9274c91aa", "97b6b97bd19801ec9210c965cc920e", "97bcf97c359801ec95f8c965cc920f", + "97bd097bd097c35b0b6fc920fb0722", "9778397bd097c36b0b6fc9274c91aa", "97b6b97bd19801ec9210c965cc920e", + "97bcf97c359801ec95f8c965cc920f", "97bd097bd097c35b0b6fc920fb0722", "9778397bd097c36b0b6fc9274c91aa", + "97b6b97bd19801ec9210c965cc920e", "97bcf97c359801ec95f8c965cc920f", "97bd097bd07f595b0b6fc920fb0722", + "9778397bd097c36b0b6fc9210c8dc2", "9778397bd19801ec9210c9274c920e", "97b6b97bd19801ec95f8c965cc920f", + "97bd07f5307f595b0b0bc920fb0722", "7f0e397bd097c36b0b6fc9210c8dc2", "9778397bd097c36c9210c9274c920e", + "97b6b97bd19801ec95f8c965cc920f", "97bd07f5307f595b0b0bc920fb0722", "7f0e397bd097c36b0b6fc9210c8dc2", + "9778397bd097c36c9210c9274c91aa", "97b6b97bd19801ec9210c965cc920e", "97bd07f1487f595b0b0bc920fb0722", + "7f0e397bd097c36b0b6fc9210c8dc2", "9778397bd097c36b0b6fc9274c91aa", "97b6b97bd19801ec9210c965cc920e", + "97bcf7f1487f595b0b0bb0b6fb0722", "7f0e397bd097c35b0b6fc920fb0722", "9778397bd097c36b0b6fc9274c91aa", + "97b6b97bd19801ec9210c965cc920e", "97bcf7f1487f595b0b0bb0b6fb0722", "7f0e397bd097c35b0b6fc920fb0722", + "9778397bd097c36b0b6fc9274c91aa", "97b6b97bd19801ec9210c965cc920e", "97bcf7f1487f531b0b0bb0b6fb0722", + "7f0e397bd097c35b0b6fc920fb0722", "9778397bd097c36b0b6fc9274c91aa", "97b6b97bd19801ec9210c965cc920e", + "97bcf7f1487f531b0b0bb0b6fb0722", "7f0e397bd07f595b0b6fc920fb0722", "9778397bd097c36b0b6fc9274c91aa", + "97b6b97bd19801ec9210c9274c920e", "97bcf7f0e47f531b0b0bb0b6fb0722", "7f0e397bd07f595b0b0bc920fb0722", + "9778397bd097c36b0b6fc9210c91aa", "97b6b97bd197c36c9210c9274c920e", "97bcf7f0e47f531b0b0bb0b6fb0722", + "7f0e397bd07f595b0b0bc920fb0722", "9778397bd097c36b0b6fc9210c8dc2", "9778397bd097c36c9210c9274c920e", + "97b6b7f0e47f531b0723b0b6fb0722", "7f0e37f5307f595b0b0bc920fb0722", "7f0e397bd097c36b0b6fc9210c8dc2", + "9778397bd097c36b0b70c9274c91aa", "97b6b7f0e47f531b0723b0b6fb0721", "7f0e37f1487f595b0b0bb0b6fb0722", + "7f0e397bd097c35b0b6fc9210c8dc2", "9778397bd097c36b0b6fc9274c91aa", "97b6b7f0e47f531b0723b0b6fb0721", + "7f0e27f1487f595b0b0bb0b6fb0722", "7f0e397bd097c35b0b6fc920fb0722", "9778397bd097c36b0b6fc9274c91aa", + "97b6b7f0e47f531b0723b0b6fb0721", "7f0e27f1487f531b0b0bb0b6fb0722", "7f0e397bd097c35b0b6fc920fb0722", + "9778397bd097c36b0b6fc9274c91aa", "97b6b7f0e47f531b0723b0b6fb0721", "7f0e27f1487f531b0b0bb0b6fb0722", + "7f0e397bd097c35b0b6fc920fb0722", "9778397bd097c36b0b6fc9274c91aa", "97b6b7f0e47f531b0723b0b6fb0721", + "7f0e27f1487f531b0b0bb0b6fb0722", "7f0e397bd07f595b0b0bc920fb0722", "9778397bd097c36b0b6fc9274c91aa", + "97b6b7f0e47f531b0723b0787b0721", "7f0e27f0e47f531b0b0bb0b6fb0722", "7f0e397bd07f595b0b0bc920fb0722", + "9778397bd097c36b0b6fc9210c91aa", "97b6b7f0e47f149b0723b0787b0721", "7f0e27f0e47f531b0723b0b6fb0722", + "7f0e397bd07f595b0b0bc920fb0722", "9778397bd097c36b0b6fc9210c8dc2", "977837f0e37f149b0723b0787b0721", + "7f07e7f0e47f531b0723b0b6fb0722", "7f0e37f5307f595b0b0bc920fb0722", "7f0e397bd097c35b0b6fc9210c8dc2", + "977837f0e37f14998082b0787b0721", "7f07e7f0e47f531b0723b0b6fb0721", "7f0e37f1487f595b0b0bb0b6fb0722", + "7f0e397bd097c35b0b6fc9210c8dc2", "977837f0e37f14998082b0787b06bd", "7f07e7f0e47f531b0723b0b6fb0721", + "7f0e27f1487f531b0b0bb0b6fb0722", "7f0e397bd097c35b0b6fc920fb0722", "977837f0e37f14998082b0787b06bd", + "7f07e7f0e47f531b0723b0b6fb0721", "7f0e27f1487f531b0b0bb0b6fb0722", "7f0e397bd097c35b0b6fc920fb0722", + "977837f0e37f14998082b0787b06bd", "7f07e7f0e47f531b0723b0b6fb0721", "7f0e27f1487f531b0b0bb0b6fb0722", + "7f0e397bd07f595b0b0bc920fb0722", "977837f0e37f14998082b0787b06bd", "7f07e7f0e47f531b0723b0b6fb0721", + "7f0e27f1487f531b0b0bb0b6fb0722", "7f0e397bd07f595b0b0bc920fb0722", "977837f0e37f14998082b0787b06bd", + "7f07e7f0e47f149b0723b0787b0721", "7f0e27f0e47f531b0b0bb0b6fb0722", "7f0e397bd07f595b0b0bc920fb0722", + "977837f0e37f14998082b0723b06bd", "7f07e7f0e37f149b0723b0787b0721", "7f0e27f0e47f531b0723b0b6fb0722", + "7f0e397bd07f595b0b0bc920fb0722", "977837f0e37f14898082b0723b02d5", "7ec967f0e37f14998082b0787b0721", + "7f07e7f0e47f531b0723b0b6fb0722", "7f0e37f1487f595b0b0bb0b6fb0722", "7f0e37f0e37f14898082b0723b02d5", + "7ec967f0e37f14998082b0787b0721", "7f07e7f0e47f531b0723b0b6fb0722", "7f0e37f1487f531b0b0bb0b6fb0722", + "7f0e37f0e37f14898082b0723b02d5", "7ec967f0e37f14998082b0787b06bd", "7f07e7f0e47f531b0723b0b6fb0721", + "7f0e37f1487f531b0b0bb0b6fb0722", "7f0e37f0e37f14898082b072297c35", "7ec967f0e37f14998082b0787b06bd", + "7f07e7f0e47f531b0723b0b6fb0721", "7f0e27f1487f531b0b0bb0b6fb0722", "7f0e37f0e37f14898082b072297c35", + "7ec967f0e37f14998082b0787b06bd", "7f07e7f0e47f531b0723b0b6fb0721", "7f0e27f1487f531b0b0bb0b6fb0722", + "7f0e37f0e366aa89801eb072297c35", "7ec967f0e37f14998082b0787b06bd", "7f07e7f0e47f149b0723b0787b0721", + "7f0e27f1487f531b0b0bb0b6fb0722", "7f0e37f0e366aa89801eb072297c35", "7ec967f0e37f14998082b0723b06bd", + "7f07e7f0e47f149b0723b0787b0721", "7f0e27f0e47f531b0723b0b6fb0722", "7f0e37f0e366aa89801eb072297c35", + "7ec967f0e37f14998082b0723b06bd", "7f07e7f0e37f14998083b0787b0721", "7f0e27f0e47f531b0723b0b6fb0722", + "7f0e37f0e366aa89801eb072297c35", "7ec967f0e37f14898082b0723b02d5", "7f07e7f0e37f14998082b0787b0721", + "7f07e7f0e47f531b0723b0b6fb0722", "7f0e36665b66aa89801e9808297c35", "665f67f0e37f14898082b0723b02d5", + "7ec967f0e37f14998082b0787b0721", "7f07e7f0e47f531b0723b0b6fb0722", "7f0e36665b66a449801e9808297c35", + "665f67f0e37f14898082b0723b02d5", "7ec967f0e37f14998082b0787b06bd", "7f07e7f0e47f531b0723b0b6fb0721", + "7f0e36665b66a449801e9808297c35", "665f67f0e37f14898082b072297c35", "7ec967f0e37f14998082b0787b06bd", + "7f07e7f0e47f531b0723b0b6fb0721", "7f0e26665b66a449801e9808297c35", "665f67f0e37f1489801eb072297c35", + "7ec967f0e37f14998082b0787b06bd", "7f07e7f0e47f531b0723b0b6fb0721", "7f0e27f1487f531b0b0bb0b6fb0722"}; + + /** + * 24节气 + */ + private static final String[] TERMS = { + "小寒", "大寒", "立春", "雨水", "惊蛰", "春分", + "清明", "谷雨", "立夏", "小满", "芒种", "夏至", + "小暑", "大暑", "立秋", "处暑", "白露", "秋分", + "寒露", "霜降", "立冬", "小雪", "大雪", "冬至" + }; + + /** + * 传入公历y年获得该年第n个节气的公历日期 + * + * @param y 公历年(1900-2100) + * @param n 二十四节气中的第几个节气(1~24);从n=1(小寒)算起 + * @return getTerm(1987,3) -》4;意即1987年2月4日立春 + */ + public static int getTerm(int y, int n) { + if (y < 1900 || y > 2100) { + return -1; + } + if (n < 1 || n > 24) { + return -1; + } + + final String _table = S_TERM_INFO[y - 1900]; + Integer[] _info = new Integer[6]; + for (int i = 0; i < 6; i++) { + _info[i] = Integer.parseInt(_table.substring(i * 5, 5 * (i + 1)), 16); + } + String[] _calday = new String[24]; + for (int i = 0; i < 6; i++) { + _calday[4 * i] = _info[i].toString().substring(0, 1); + _calday[4 * i + 1] = _info[i].toString().substring(1, 3); + _calday[4 * i + 2] = _info[i].toString().substring(3, 4); + _calday[4 * i + 3] = _info[i].toString().substring(4, 6); + } + return NumberUtil.parseInt(_calday[n - 1]); + } + + /** + * 根据日期获取节气 + * @param date 日期 + * @return 返回指定日期所处的节气,若不是一个节气则返回空字符串 + */ + public static String getTerm(Date date) { + final DateTime dt = DateUtil.date(date); + return getTermInternal(dt.year(), dt.month() + 1, dt.dayOfMonth()); + } + + + /** + * 根据农历日期获取节气 + * @param chineseDate 农历日期 + * @return 返回指定农历日期所处的节气,若不是一个节气则返回空字符串 + */ + public static String getTerm(ChineseDate chineseDate) { + return chineseDate.getTerm(); + } + + /** + * 根据日期获取节气 + * @param date 日期 + * @return 返回指定日期所处的节气,若不是一个节气则返回空字符串 + */ + public static String getTerm(LocalDate date) { + return getTermInternal(date.getYear(), date.getMonthValue(), date.getDayOfMonth()); + } + + /** + * 根据年月日获取节气 + * @param year 公历年 + * @param mouth 公历月,从1开始 + * @param day 公历日,从1开始 + * @return 返回指定年月日所处的节气,若不是一个节气则返回空字符串 + */ + public static String getTerm(int year, int mouth, int day) { + return getTerm(LocalDate.of(year, mouth, day)); + } + + /** + * 根据年月日获取节气, 内部方法,不对月和日做有效校验 + * @param year 公历年 + * @param mouth 公历月,从1开始 + * @param day 公历日,从1开始 + * @return 返回指定年月日所处的节气,若不是一个节气则返回空字符串 + */ + private static String getTermInternal(int year, int mouth, int day) { + if (year < 1900 || year > 2100) { + throw new IllegalArgumentException("只支持1900-2100之间的日期获取节气"); + } + + final String termTable = S_TERM_INFO[year - 1900]; + + // 节气速查表中每5个字符含有4个节气,通过月份直接计算偏移 + final int segment = (mouth + 1) / 2 - 1; + final int termInfo = Integer.parseInt(termTable.substring(segment * 5, (segment + 1) * 5), 16); + final String termInfoStr = String.valueOf(termInfo); + + final String[] segmentTable = new String[4]; + segmentTable[0] = termInfoStr.substring(0, 1); + segmentTable[1] = termInfoStr.substring(1, 3); + segmentTable[2] = termInfoStr.substring(3, 4); + segmentTable[3] = termInfoStr.substring(4, 6); + + // 奇数月份的节气在前2个,偶数月份的节气在后两个 + final int segmentOffset = (mouth & 1) == 1 ? 0 : 2; + + if (day == Integer.parseInt(segmentTable[segmentOffset])) { + return TERMS[segment * 4 + segmentOffset]; + } + if (day == Integer.parseInt(segmentTable[segmentOffset + 1])) { + return TERMS[segment * 4 + segmentOffset + 1]; + } + return StrUtil.EMPTY; + } +} diff --git a/src/main/java/cn/hutool/core/date/chinese/package-info.java b/src/main/java/cn/hutool/core/date/chinese/package-info.java new file mode 100644 index 0000000..1310185 --- /dev/null +++ b/src/main/java/cn/hutool/core/date/chinese/package-info.java @@ -0,0 +1,7 @@ +/** + * 农历相关类汇总,包括农历月、天干地支、农历节日、24节气等 + * + * @author looly + * + */ +package cn.hutool.core.date.chinese; \ No newline at end of file diff --git a/src/main/java/cn/hutool/core/date/format/AbstractDateBasic.java b/src/main/java/cn/hutool/core/date/format/AbstractDateBasic.java new file mode 100644 index 0000000..2ea4cf1 --- /dev/null +++ b/src/main/java/cn/hutool/core/date/format/AbstractDateBasic.java @@ -0,0 +1,64 @@ +package cn.hutool.core.date.format; + +import java.io.Serializable; +import java.util.Locale; +import java.util.TimeZone; + +public abstract class AbstractDateBasic implements DateBasic, Serializable { + private static final long serialVersionUID = 6333136319870641818L; + + /** The pattern */ + protected final String pattern; + /** The time zone. */ + protected final TimeZone timeZone; + /** The locale. */ + protected final Locale locale; + + /** + * 构造,内部使用 + * @param pattern 使用{@link java.text.SimpleDateFormat} 相同的日期格式 + * @param timeZone 非空时区{@link TimeZone} + * @param locale 非空{@link Locale} 日期地理位置 + */ + protected AbstractDateBasic(final String pattern, final TimeZone timeZone, final Locale locale) { + this.pattern = pattern; + this.timeZone = timeZone; + this.locale = locale; + } + + // ----------------------------------------------------------------------- Accessors + @Override + public String getPattern() { + return pattern; + } + + @Override + public TimeZone getTimeZone() { + return timeZone; + } + + @Override + public Locale getLocale() { + return locale; + } + + // ----------------------------------------------------------------------- Basics + @Override + public boolean equals(final Object obj) { + if (!(obj instanceof FastDatePrinter)) { + return false; + } + final AbstractDateBasic other = (AbstractDateBasic) obj; + return pattern.equals(other.pattern) && timeZone.equals(other.timeZone) && locale.equals(other.locale); + } + + @Override + public int hashCode() { + return pattern.hashCode() + 13 * (timeZone.hashCode() + 13 * locale.hashCode()); + } + + @Override + public String toString() { + return "FastDatePrinter[" + pattern + "," + locale + "," + timeZone.getID() + "]"; + } +} diff --git a/src/main/java/cn/hutool/core/date/format/DateBasic.java b/src/main/java/cn/hutool/core/date/format/DateBasic.java new file mode 100644 index 0000000..e8c7c42 --- /dev/null +++ b/src/main/java/cn/hutool/core/date/format/DateBasic.java @@ -0,0 +1,34 @@ +package cn.hutool.core.date.format; + +import java.util.Locale; +import java.util.TimeZone; + +/** + * 日期基本信息获取接口 + * + * @author Looly + * @since 2.16.2 + */ +public interface DateBasic { + + /** + * 获得日期格式化或者转换的格式 + * + * @return {@link java.text.SimpleDateFormat}兼容的格式 + */ + String getPattern(); + + /** + * 获得时区 + * + * @return {@link TimeZone} + */ + TimeZone getTimeZone(); + + /** + * 获得 日期地理位置 + * + * @return {@link Locale} + */ + Locale getLocale(); +} diff --git a/src/main/java/cn/hutool/core/date/format/DateParser.java b/src/main/java/cn/hutool/core/date/format/DateParser.java new file mode 100644 index 0000000..5668f57 --- /dev/null +++ b/src/main/java/cn/hutool/core/date/format/DateParser.java @@ -0,0 +1,72 @@ +package cn.hutool.core.date.format; + +import java.text.ParseException; +import java.text.ParsePosition; +import java.util.Calendar; +import java.util.Date; + +/** + * 日期解析接口,用于解析日期字符串为 {@link Date} 对象
+ * Thanks to Apache Commons Lang 3.5 + * @since 2.16.2 + */ +public interface DateParser extends DateBasic{ + + /** + * 将日期字符串解析并转换为 {@link Date} 对象
+ * 等价于 {@link java.text.DateFormat#parse(String)} + * + * @param source 日期字符串 + * @return {@link Date} + * @throws ParseException 转换异常,被转换的字符串格式错误。 + */ + Date parse(String source) throws ParseException; + + /** + * 将日期字符串解析并转换为 {@link Date} 对象
+ * 等价于 {@link java.text.DateFormat#parse(String, ParsePosition)} + * + * @param source 日期字符串 + * @param pos {@link ParsePosition} + * @return {@link Date} + */ + Date parse(String source, ParsePosition pos); + + /** + * 根据给定格式更新{@link Calendar} + * Upon success, the ParsePosition index is updated to indicate how much of the source text was consumed. + * Not all source text needs to be consumed. + * Upon parse failure, ParsePosition error index is updated to the offset of the source text which does not match the supplied format. + * + * @param source 被转换的日期字符串 + * @param pos 定义开始转换的位置,转换结束后更新转换到的位置 + * @param calendar The calendar into which to set parsed fields. + * @return true, if source has been parsed (pos parsePosition is updated); otherwise false (and pos errorIndex is updated) + * @throws IllegalArgumentException when Calendar has been set to be not lenient, and a parsed field is out of range. + */ + boolean parse(String source, ParsePosition pos, Calendar calendar); + + /** + * 将日期字符串解析并转换为 {@link Date} 对象
+ * + * @param source A {@code String} whose beginning should be parsed. + * @return a {@code java.util.Date} object + * @throws ParseException if the beginning of the specified string cannot be parsed. + * @see java.text.DateFormat#parseObject(String) + */ + default Object parseObject(String source) throws ParseException{ + return parse(source); + } + + /** + * 根据 {@link ParsePosition} 给定将日期字符串解析并转换为 {@link Date} 对象
+ * + * @param source A {@code String} whose beginning should be parsed. + * @param pos the parse position + * @return a {@code java.util.Date} object + * @see java.text.DateFormat#parseObject(String, ParsePosition) + */ + default Object parseObject(String source, ParsePosition pos){ + return parse(source, pos); + } +} diff --git a/src/main/java/cn/hutool/core/date/format/DatePrinter.java b/src/main/java/cn/hutool/core/date/format/DatePrinter.java new file mode 100644 index 0000000..92df7c5 --- /dev/null +++ b/src/main/java/cn/hutool/core/date/format/DatePrinter.java @@ -0,0 +1,78 @@ +package cn.hutool.core.date.format; + +import java.util.Calendar; +import java.util.Date; + +/** + * 日期格式化输出接口
+ * Thanks to Apache Commons Lang 3.5 + * @author Looly + * @since 2.16.2 + */ +public interface DatePrinter extends DateBasic { + + /** + * 格式化日期表示的毫秒数 + * + * @param millis 日期毫秒数 + * @return the formatted string + * @since 2.1 + */ + String format(long millis); + + /** + * 使用 {@code GregorianCalendar} 格式化 {@code Date} + * + * @param date 日期 {@link Date} + * @return 格式化后的字符串 + */ + String format(Date date); + + /** + *

+ * Formats a {@code Calendar} object. + *

+ * 格式化 {@link Calendar} + * + * @param calendar {@link Calendar} + * @return 格式化后的字符串 + */ + String format(Calendar calendar); + + /** + *

+ * Formats a millisecond {@code long} value into the supplied {@code Appendable}. + *

+ * + * @param millis the millisecond value to format + * @param buf the buffer to format into + * @param the Appendable class type, usually StringBuilder or StringBuffer. + * @return the specified string buffer + */ + B format(long millis, B buf); + + /** + *

+ * Formats a {@code Date} object into the supplied {@code Appendable} using a {@code GregorianCalendar}. + *

+ * + * @param date the date to format + * @param buf the buffer to format into + * @param the Appendable class type, usually StringBuilder or StringBuffer. + * @return the specified string buffer + */ + B format(Date date, B buf); + + /** + *

+ * Formats a {@code Calendar} object into the supplied {@code Appendable}. + *

+ * The TimeZone set on the Calendar is only used to adjust the time offset. The TimeZone specified during the construction of the Parser will determine the TimeZone used in the formatted string. + * + * @param calendar the calendar to format + * @param buf the buffer to format into + * @param the Appendable class type, usually StringBuilder or StringBuffer. + * @return the specified string buffer + */ + B format(Calendar calendar, B buf); +} diff --git a/src/main/java/cn/hutool/core/date/format/FastDateFormat.java b/src/main/java/cn/hutool/core/date/format/FastDateFormat.java new file mode 100644 index 0000000..b8bc9e4 --- /dev/null +++ b/src/main/java/cn/hutool/core/date/format/FastDateFormat.java @@ -0,0 +1,420 @@ +package cn.hutool.core.date.format; + +import cn.hutool.core.date.DatePattern; + +import java.text.DateFormat; +import java.text.FieldPosition; +import java.text.Format; +import java.text.ParseException; +import java.text.ParsePosition; +import java.time.format.DateTimeFormatter; +import java.util.Calendar; +import java.util.Date; +import java.util.Locale; +import java.util.TimeZone; + +/** + *

+ * FastDateFormat 是一个线程安全的 {@link java.text.SimpleDateFormat} 实现。 + *

+ * + *

+ * 通过以下静态方法获得此对象:
+ * {@link #getInstance(String, TimeZone, Locale)}
+ * {@link #getDateInstance(int, TimeZone, Locale)}
+ * {@link #getTimeInstance(int, TimeZone, Locale)}
+ * {@link #getDateTimeInstance(int, int, TimeZone, Locale)} + *

+ * + * Thanks to Apache Commons Lang 3.5 + * @since 2.16.2 + */ +public class FastDateFormat extends Format implements DateParser, DatePrinter { + private static final long serialVersionUID = 8097890768636183236L; + + /** FULL locale dependent date or time style. */ + public static final int FULL = DateFormat.FULL; + /** LONG locale dependent date or time style. */ + public static final int LONG = DateFormat.LONG; + /** MEDIUM locale dependent date or time style. */ + public static final int MEDIUM = DateFormat.MEDIUM; + /** SHORT locale dependent date or time style. */ + public static final int SHORT = DateFormat.SHORT; + + private static final FormatCache CACHE = new FormatCache(){ + @Override + protected FastDateFormat createInstance(final String pattern, final TimeZone timeZone, final Locale locale) { + return new FastDateFormat(pattern, timeZone, locale); + } + }; + + private final FastDatePrinter printer; + private final FastDateParser parser; + + // ----------------------------------------------------------------------- + /** + * 获得 FastDateFormat实例,使用默认格式和地区 + * + * @return FastDateFormat + */ + public static FastDateFormat getInstance() { + return CACHE.getInstance(); + } + + /** + * 获得 FastDateFormat 实例,使用默认地区
+ * 支持缓存 + * + * @param pattern 使用{@link java.text.SimpleDateFormat} 相同的日期格式 + * @return FastDateFormat + * @throws IllegalArgumentException 日期格式问题 + */ + public static FastDateFormat getInstance(final String pattern) { + return CACHE.getInstance(pattern, null, null); + } + + /** + * 获得 FastDateFormat 实例
+ * 支持缓存 + * + * @param pattern 使用{@link java.text.SimpleDateFormat} 相同的日期格式 + * @param timeZone 时区{@link TimeZone} + * @return FastDateFormat + * @throws IllegalArgumentException 日期格式问题 + */ + public static FastDateFormat getInstance(final String pattern, final TimeZone timeZone) { + return CACHE.getInstance(pattern, timeZone, null); + } + + /** + * 获得 FastDateFormat 实例
+ * 支持缓存 + * + * @param pattern 使用{@link java.text.SimpleDateFormat} 相同的日期格式 + * @param locale {@link Locale} 日期地理位置 + * @return FastDateFormat + * @throws IllegalArgumentException 日期格式问题 + */ + public static FastDateFormat getInstance(final String pattern, final Locale locale) { + return CACHE.getInstance(pattern, null, locale); + } + + /** + * 获得 FastDateFormat 实例
+ * 支持缓存 + * + * @param pattern 使用{@link java.text.SimpleDateFormat} 相同的日期格式 + * @param timeZone 时区{@link TimeZone} + * @param locale {@link Locale} 日期地理位置 + * @return FastDateFormat + * @throws IllegalArgumentException 日期格式问题 + */ + public static FastDateFormat getInstance(final String pattern, final TimeZone timeZone, final Locale locale) { + return CACHE.getInstance(pattern, timeZone, locale); + } + + // ----------------------------------------------------------------------- + /** + * 获得 FastDateFormat 实例
+ * 支持缓存 + * + * @param style date style: FULL, LONG, MEDIUM, or SHORT + * @return 本地化 FastDateFormat + */ + public static FastDateFormat getDateInstance(final int style) { + return CACHE.getDateInstance(style, null, null); + } + + /** + * 获得 FastDateFormat 实例
+ * 支持缓存 + * + * @param style date style: FULL, LONG, MEDIUM, or SHORT + * @param locale {@link Locale} 日期地理位置 + * @return 本地化 FastDateFormat + */ + public static FastDateFormat getDateInstance(final int style, final Locale locale) { + return CACHE.getDateInstance(style, null, locale); + } + + /** + * 获得 FastDateFormat 实例
+ * 支持缓存 + * + * @param style date style: FULL, LONG, MEDIUM, or SHORT + * @param timeZone 时区{@link TimeZone} + * @return 本地化 FastDateFormat + */ + public static FastDateFormat getDateInstance(final int style, final TimeZone timeZone) { + return CACHE.getDateInstance(style, timeZone, null); + } + + /** + * 获得 FastDateFormat 实例
+ * 支持缓存 + * + * @param style date style: FULL, LONG, MEDIUM, or SHORT + * @param timeZone 时区{@link TimeZone} + * @param locale {@link Locale} 日期地理位置 + * @return 本地化 FastDateFormat + */ + public static FastDateFormat getDateInstance(final int style, final TimeZone timeZone, final Locale locale) { + return CACHE.getDateInstance(style, timeZone, locale); + } + + // ----------------------------------------------------------------------- + /** + * 获得 FastDateFormat 实例
+ * 支持缓存 + * + * @param style time style: FULL, LONG, MEDIUM, or SHORT + * @return 本地化 FastDateFormat + */ + public static FastDateFormat getTimeInstance(final int style) { + return CACHE.getTimeInstance(style, null, null); + } + + /** + * 获得 FastDateFormat 实例
+ * 支持缓存 + * + * @param style time style: FULL, LONG, MEDIUM, or SHORT + * @param locale {@link Locale} 日期地理位置 + * @return 本地化 FastDateFormat + */ + public static FastDateFormat getTimeInstance(final int style, final Locale locale) { + return CACHE.getTimeInstance(style, null, locale); + } + + /** + * 获得 FastDateFormat 实例
+ * 支持缓存 + * + * @param style time style: FULL, LONG, MEDIUM, or SHORT + * @param timeZone optional time zone, overrides time zone of formatted time + * @return 本地化 FastDateFormat + */ + public static FastDateFormat getTimeInstance(final int style, final TimeZone timeZone) { + return CACHE.getTimeInstance(style, timeZone, null); + } + + /** + * 获得 FastDateFormat 实例
+ * 支持缓存 + * + * @param style time style: FULL, LONG, MEDIUM, or SHORT + * @param timeZone optional time zone, overrides time zone of formatted time + * @param locale {@link Locale} 日期地理位置 + * @return 本地化 FastDateFormat + */ + public static FastDateFormat getTimeInstance(final int style, final TimeZone timeZone, final Locale locale) { + return CACHE.getTimeInstance(style, timeZone, locale); + } + + // ----------------------------------------------------------------------- + /** + * 获得 FastDateFormat 实例
+ * 支持缓存 + * + * @param dateStyle date style: FULL, LONG, MEDIUM, or SHORT + * @param timeStyle time style: FULL, LONG, MEDIUM, or SHORT + * @return 本地化 FastDateFormat + */ + public static FastDateFormat getDateTimeInstance(final int dateStyle, final int timeStyle) { + return CACHE.getDateTimeInstance(dateStyle, timeStyle, null, null); + } + + /** + * 获得 FastDateFormat 实例
+ * 支持缓存 + * + * @param dateStyle date style: FULL, LONG, MEDIUM, or SHORT + * @param timeStyle time style: FULL, LONG, MEDIUM, or SHORT + * @param locale {@link Locale} 日期地理位置 + * @return 本地化 FastDateFormat + */ + public static FastDateFormat getDateTimeInstance(final int dateStyle, final int timeStyle, final Locale locale) { + return CACHE.getDateTimeInstance(dateStyle, timeStyle, null, locale); + } + + /** + * 获得 FastDateFormat 实例
+ * 支持缓存 + * + * @param dateStyle date style: FULL, LONG, MEDIUM, or SHORT + * @param timeStyle time style: FULL, LONG, MEDIUM, or SHORT + * @param timeZone 时区{@link TimeZone} + * @return 本地化 FastDateFormat + */ + public static FastDateFormat getDateTimeInstance(final int dateStyle, final int timeStyle, final TimeZone timeZone) { + return getDateTimeInstance(dateStyle, timeStyle, timeZone, null); + } + + /** + * 获得 FastDateFormat 实例
+ * 支持缓存 + * + * @param dateStyle date style: FULL, LONG, MEDIUM, or SHORT + * @param timeStyle time style: FULL, LONG, MEDIUM, or SHORT + * @param timeZone 时区{@link TimeZone} + * @param locale {@link Locale} 日期地理位置 + * @return 本地化 FastDateFormat + */ + public static FastDateFormat getDateTimeInstance(final int dateStyle, final int timeStyle, final TimeZone timeZone, final Locale locale) { + return CACHE.getDateTimeInstance(dateStyle, timeStyle, timeZone, locale); + } + + // ----------------------------------------------------------------------- Constructor start + /** + * 构造 + * + * @param pattern 使用{@link java.text.SimpleDateFormat} 相同的日期格式 + * @param timeZone 非空时区 {@link TimeZone} + * @param locale {@link Locale} 日期地理位置 + * @throws NullPointerException if pattern, timeZone, or locale is null. + */ + protected FastDateFormat(final String pattern, final TimeZone timeZone, final Locale locale) { + this(pattern, timeZone, locale, null); + } + + /** + * 构造 + * + * @param pattern 使用{@link java.text.SimpleDateFormat} 相同的日期格式 + * @param timeZone 非空时区 {@link TimeZone} + * @param locale {@link Locale} 日期地理位置 + * @param centuryStart The start of the 100 year period to use as the "default century" for 2 digit year parsing. If centuryStart is null, defaults to now - 80 years + * @throws NullPointerException if pattern, timeZone, or locale is null. + */ + protected FastDateFormat(final String pattern, final TimeZone timeZone, final Locale locale, final Date centuryStart) { + printer = new FastDatePrinter(pattern, timeZone, locale); + parser = new FastDateParser(pattern, timeZone, locale, centuryStart); + } + // ----------------------------------------------------------------------- Constructor end + + // ----------------------------------------------------------------------- Format methods + @Override + public StringBuffer format(final Object obj, final StringBuffer toAppendTo, final FieldPosition pos) { + return toAppendTo.append(printer.format(obj)); + } + + @Override + public String format(final long millis) { + return printer.format(millis); + } + + @Override + public String format(final Date date) { + return printer.format(date); + } + + @Override + public String format(final Calendar calendar) { + return printer.format(calendar); + } + + @Override + public B format(final long millis, final B buf) { + return printer.format(millis, buf); + } + + @Override + public B format(final Date date, final B buf) { + return printer.format(date, buf); + } + + @Override + public B format(final Calendar calendar, final B buf) { + return printer.format(calendar, buf); + } + + // ----------------------------------------------------------------------- Parsing + @Override + public Date parse(final String source) throws ParseException { + return parser.parse(source); + } + + @Override + public Date parse(final String source, final ParsePosition pos) { + return parser.parse(source, pos); + } + + @Override + public boolean parse(final String source, final ParsePosition pos, final Calendar calendar) { + return parser.parse(source, pos, calendar); + } + + @Override + public Object parseObject(final String source, final ParsePosition pos) { + return parser.parseObject(source, pos); + } + + // ----------------------------------------------------------------------- Accessors + @Override + public String getPattern() { + return printer.getPattern(); + } + + @Override + public TimeZone getTimeZone() { + return printer.getTimeZone(); + } + + @Override + public Locale getLocale() { + return printer.getLocale(); + } + + /** + *估算生成的日期字符串长度
+ * 实际生成的字符串长度小于或等于此值 + * + * @return 日期字符串长度 + */ + public int getMaxLengthEstimate() { + return printer.getMaxLengthEstimate(); + } + + // convert DateTimeFormatter + // ----------------------------------------------------------------------- + + /** + * 便捷获取 DateTimeFormatter + * 由于 {@link DatePattern} 很大一部分的格式没有提供 {@link DateTimeFormatter},因此这里提供快捷获取方式 + * @return DateTimeFormatter + * @author dazer neusoft + * @since 5.6.4 + */ + public DateTimeFormatter getDateTimeFormatter() { + DateTimeFormatter formatter = DateTimeFormatter.ofPattern(this.getPattern()); + if (this.getLocale() != null) { + formatter = formatter.withLocale(this.getLocale()); + } + if (this.getTimeZone() != null) { + formatter = formatter.withZone(this.getTimeZone().toZoneId()); + } + return formatter; + } + + // Basics + // ----------------------------------------------------------------------- + @Override + public boolean equals(final Object obj) { + if (!(obj instanceof FastDateFormat)) { + return false; + } + final FastDateFormat other = (FastDateFormat) obj; + // no need to check parser, as it has same invariants as printer + return printer.equals(other.printer); + } + + @Override + public int hashCode() { + return printer.hashCode(); + } + + @Override + public String toString() { + return "FastDateFormat[" + printer.getPattern() + "," + printer.getLocale() + "," + printer.getTimeZone().getID() + "]"; + } +} diff --git a/src/main/java/cn/hutool/core/date/format/FastDateParser.java b/src/main/java/cn/hutool/core/date/format/FastDateParser.java new file mode 100644 index 0000000..2505935 --- /dev/null +++ b/src/main/java/cn/hutool/core/date/format/FastDateParser.java @@ -0,0 +1,810 @@ +package cn.hutool.core.date.format; + +import cn.hutool.core.map.SafeConcurrentHashMap; + +import java.io.IOException; +import java.io.ObjectInputStream; +import java.text.DateFormatSymbols; +import java.text.ParseException; +import java.text.ParsePosition; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Comparator; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.ListIterator; +import java.util.Locale; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.TimeZone; +import java.util.TreeSet; +import java.util.concurrent.ConcurrentMap; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * {@link java.text.SimpleDateFormat} 的线程安全版本,用于解析日期字符串并转换为 {@link Date} 对象
+ * Thanks to Apache Commons Lang 3.5 + * + * @see FastDatePrinter + * @since 2.16.2 + */ +public class FastDateParser extends AbstractDateBasic implements DateParser { + private static final long serialVersionUID = -3199383897950947498L; + + static final Locale JAPANESE_IMPERIAL = new Locale("ja", "JP", "JP"); + + /** + * 世纪:2000年前为19, 之后为20 + */ + private final int century; + private final int startYear; + + // derived fields + private transient List patterns; + + // comparator used to sort regex alternatives + // alternatives should be ordered longer first, and shorter last. ('february' before 'feb') + // all entries must be lowercase by locale. + private static final Comparator LONGER_FIRST_LOWERCASE = Comparator.reverseOrder(); + + /** + *

+ * Constructs a new FastDateParser. + *

+ *

+ * Use {@link FastDateFormat#getInstance(String, TimeZone, Locale)} or another variation of the factory methods of {@link FastDateFormat} to get a cached FastDateParser instance. + * + * @param pattern non-null {@link java.text.SimpleDateFormat} compatible pattern + * @param timeZone non-null time zone to use + * @param locale non-null locale + */ + public FastDateParser(String pattern, TimeZone timeZone, Locale locale) { + this(pattern, timeZone, locale, null); + } + + /** + *

+ * Constructs a new FastDateParser. + *

+ * + * @param pattern non-null {@link java.text.SimpleDateFormat} compatible pattern + * @param timeZone non-null time zone to use + * @param locale non-null locale + * @param centuryStart The start of the century for 2 digit year parsing + */ + public FastDateParser(final String pattern, final TimeZone timeZone, final Locale locale, final Date centuryStart) { + super(pattern, timeZone, locale); + final Calendar definingCalendar = Calendar.getInstance(timeZone, locale); + + int centuryStartYear; + if (centuryStart != null) { + definingCalendar.setTime(centuryStart); + centuryStartYear = definingCalendar.get(Calendar.YEAR); + } else if (locale.equals(JAPANESE_IMPERIAL)) { + centuryStartYear = 0; + } else { + // from 80 years ago to 20 years from now + definingCalendar.setTime(new Date()); + centuryStartYear = definingCalendar.get(Calendar.YEAR) - 80; + } + century = centuryStartYear / 100 * 100; + startYear = centuryStartYear - century; + + init(definingCalendar); + } + + /** + * Initialize derived fields from defining fields. This is called from constructor and from readObject (de-serialization) + * + * @param definingCalendar the {@link Calendar} instance used to initialize this FastDateParser + */ + private void init(final Calendar definingCalendar) { + patterns = new ArrayList<>(); + + final StrategyParser fm = new StrategyParser(definingCalendar); + for (; ; ) { + final StrategyAndWidth field = fm.getNextStrategy(); + if (field == null) { + break; + } + patterns.add(field); + } + } + + // helper classes to parse the format string + // ----------------------------------------------------------------------- + + /** + * Holds strategy and field width + */ + private static class StrategyAndWidth { + final Strategy strategy; + final int width; + + StrategyAndWidth(final Strategy strategy, final int width) { + this.strategy = strategy; + this.width = width; + } + + int getMaxWidth(final ListIterator lt) { + if (!strategy.isNumber() || !lt.hasNext()) { + return 0; + } + final Strategy nextStrategy = lt.next().strategy; + lt.previous(); + return nextStrategy.isNumber() ? width : 0; + } + } + + /** + * Parse format into Strategies + */ + private class StrategyParser { + final private Calendar definingCalendar; + private int currentIdx; + + StrategyParser(final Calendar definingCalendar) { + this.definingCalendar = definingCalendar; + } + + StrategyAndWidth getNextStrategy() { + if (currentIdx >= pattern.length()) { + return null; + } + + final char c = pattern.charAt(currentIdx); + if (isFormatLetter(c)) { + return letterPattern(c); + } + return literal(); + } + + private StrategyAndWidth letterPattern(final char c) { + final int begin = currentIdx; + while (++currentIdx < pattern.length()) { + if (pattern.charAt(currentIdx) != c) { + break; + } + } + + final int width = currentIdx - begin; + return new StrategyAndWidth(getStrategy(c, width, definingCalendar), width); + } + + private StrategyAndWidth literal() { + boolean activeQuote = false; + + final StringBuilder sb = new StringBuilder(); + while (currentIdx < pattern.length()) { + final char c = pattern.charAt(currentIdx); + if (!activeQuote && isFormatLetter(c)) { + break; + } else if (c == '\'' && (++currentIdx == pattern.length() || pattern.charAt(currentIdx) != '\'')) { + activeQuote = !activeQuote; + continue; + } + ++currentIdx; + sb.append(c); + } + + if (activeQuote) { + throw new IllegalArgumentException("Unterminated quote"); + } + + final String formatField = sb.toString(); + return new StrategyAndWidth(new CopyQuotedStrategy(formatField), formatField.length()); + } + } + + private static boolean isFormatLetter(final char c) { + return c >= 'A' && c <= 'Z' || c >= 'a' && c <= 'z'; + } + + // Serializing + // ----------------------------------------------------------------------- + + /** + * Create the object after serialization. This implementation reinitializes the transient properties. + * + * @param in ObjectInputStream from which the object is being deserialized. + * @throws IOException if there is an IO issue. + * @throws ClassNotFoundException if a class cannot be found. + */ + private void readObject(final ObjectInputStream in) throws IOException, ClassNotFoundException { + in.defaultReadObject(); + + final Calendar definingCalendar = Calendar.getInstance(timeZone, locale); + init(definingCalendar); + } + + @Override + public Date parse(String source) throws ParseException { + final ParsePosition pp = new ParsePosition(0); + final Date date = parse(source, pp); + if (date == null) { + // Add a note re supported date range + if (locale.equals(JAPANESE_IMPERIAL)) { + throw new ParseException("(The " + locale + " locale does not support dates before 1868 AD)\n" + + "Unparseable date: \"" + source, pp.getErrorIndex()); + } + throw new ParseException("Unparseable date: " + source, pp.getErrorIndex()); + } + return date; + } + + @Override + public Date parse(String source, ParsePosition pos) { + // timing tests indicate getting new instance is 19% faster than cloning + final Calendar cal = Calendar.getInstance(timeZone, locale); + cal.clear(); + + return parse(source, pos, cal) ? cal.getTime() : null; + } + + @Override + public boolean parse(String source, ParsePosition pos, Calendar calendar) { + final ListIterator lt = patterns.listIterator(); + while (lt.hasNext()) { + final StrategyAndWidth strategyAndWidth = lt.next(); + final int maxWidth = strategyAndWidth.getMaxWidth(lt); + if (!strategyAndWidth.strategy.parse(this, calendar, source, pos, maxWidth)) { + return false; + } + } + return true; + } + + // Support for strategies + // ----------------------------------------------------------------------- + + private static StringBuilder simpleQuote(final StringBuilder sb, final String value) { + for (int i = 0; i < value.length(); ++i) { + final char c = value.charAt(i); + switch (c) { + case '\\': + case '^': + case '$': + case '.': + case '|': + case '?': + case '*': + case '+': + case '(': + case ')': + case '[': + case '{': + sb.append('\\'); + default: + sb.append(c); + } + } + return sb; + } + + /** + * Get the short and long values displayed for a field + * + * @param cal The calendar to obtain the short and long values + * @param locale The locale of display names + * @param field The field of interest + * @param regex The regular expression to build + * @return The map of string display names to field values + */ + private static Map appendDisplayNames(final Calendar cal, final Locale locale, final int field, final StringBuilder regex) { + final Map values = new HashMap<>(); + + final Map displayNames = cal.getDisplayNames(field, Calendar.ALL_STYLES, locale); + final TreeSet sorted = new TreeSet<>(LONGER_FIRST_LOWERCASE); + for (final Map.Entry displayName : displayNames.entrySet()) { + final String key = displayName.getKey().toLowerCase(locale); + if (sorted.add(key)) { + values.put(key, displayName.getValue()); + } + } + for (final String symbol : sorted) { + simpleQuote(regex, symbol).append('|'); + } + return values; + } + + /** + * 使用当前的世纪调整两位数年份为四位数年份 + * + * @param twoDigitYear 两位数年份 + * @return A value between centuryStart(inclusive) to centuryStart+100(exclusive) + */ + private int adjustYear(final int twoDigitYear) { + final int trial = century + twoDigitYear; + return twoDigitYear >= startYear ? trial : trial + 100; + } + + /** + * 单个日期字段的分析策略 + */ + private static abstract class Strategy { + /** + * Is this field a number? The default implementation returns false. + * + * @return true, if field is a number + */ + boolean isNumber() { + return false; + } + + abstract boolean parse(FastDateParser parser, Calendar calendar, String source, ParsePosition pos, int maxWidth); + } + + /** + * A strategy to parse a single field from the parsing pattern + */ + private static abstract class PatternStrategy extends Strategy { + + private Pattern pattern; + + void createPattern(final StringBuilder regex) { + createPattern(regex.toString()); + } + + void createPattern(final String regex) { + this.pattern = Pattern.compile(regex); + } + + /** + * Is this field a number? The default implementation returns false. + * + * @return true, if field is a number + */ + @Override + boolean isNumber() { + return false; + } + + @Override + boolean parse(final FastDateParser parser, final Calendar calendar, final String source, final ParsePosition pos, final int maxWidth) { + final Matcher matcher = pattern.matcher(source.substring(pos.getIndex())); + if (!matcher.lookingAt()) { + pos.setErrorIndex(pos.getIndex()); + return false; + } + pos.setIndex(pos.getIndex() + matcher.end(1)); + setCalendar(parser, calendar, matcher.group(1)); + return true; + } + + abstract void setCalendar(FastDateParser parser, Calendar cal, String value); + } + + /** + * Obtain a Strategy given a field from a SimpleDateFormat pattern + * + * @param f 格式 + * @param width 长度 + * @param definingCalendar The calendar to obtain the short and long values + * @return The Strategy that will handle parsing for the field + */ + private Strategy getStrategy(final char f, final int width, final Calendar definingCalendar) { + switch (f) { + default: + throw new IllegalArgumentException("Format '" + f + "' not supported"); + case 'D': + return DAY_OF_YEAR_STRATEGY; + case 'E': + return getLocaleSpecificStrategy(Calendar.DAY_OF_WEEK, definingCalendar); + case 'F': + return DAY_OF_WEEK_IN_MONTH_STRATEGY; + case 'G': + return getLocaleSpecificStrategy(Calendar.ERA, definingCalendar); + case 'H': // Hour in day (0-23) + return HOUR_OF_DAY_STRATEGY; + case 'K': // Hour in am/pm (0-11) + return HOUR_STRATEGY; + case 'M': + return width >= 3 ? getLocaleSpecificStrategy(Calendar.MONTH, definingCalendar) : NUMBER_MONTH_STRATEGY; + case 'S': + return MILLISECOND_STRATEGY; + case 'W': + return WEEK_OF_MONTH_STRATEGY; + case 'a': + return getLocaleSpecificStrategy(Calendar.AM_PM, definingCalendar); + case 'd': + return DAY_OF_MONTH_STRATEGY; + case 'h': // Hour in am/pm (1-12), i.e. midday/midnight is 12, not 0 + return HOUR12_STRATEGY; + case 'k': // Hour in day (1-24), i.e. midnight is 24, not 0 + return HOUR24_OF_DAY_STRATEGY; + case 'm': + return MINUTE_STRATEGY; + case 's': + return SECOND_STRATEGY; + case 'u': + return DAY_OF_WEEK_STRATEGY; + case 'w': + return WEEK_OF_YEAR_STRATEGY; + case 'y': + case 'Y': + return width > 2 ? LITERAL_YEAR_STRATEGY : ABBREVIATED_YEAR_STRATEGY; + case 'X': + return ISO8601TimeZoneStrategy.getStrategy(width); + case 'Z': + if (width == 2) { + return ISO8601TimeZoneStrategy.ISO_8601_3_STRATEGY; + } + //$FALL-THROUGH$ + case 'z': + return getLocaleSpecificStrategy(Calendar.ZONE_OFFSET, definingCalendar); + } + } + + @SuppressWarnings("unchecked") // OK because we are creating an array with no entries + private static final ConcurrentMap[] CACHES = new ConcurrentMap[Calendar.FIELD_COUNT]; + + /** + * Get a cache of Strategies for a particular field + * + * @param field The Calendar field + * @return a cache of Locale to Strategy + */ + private static ConcurrentMap getCache(final int field) { + synchronized (CACHES) { + if (CACHES[field] == null) { + CACHES[field] = new SafeConcurrentHashMap<>(3); + } + return CACHES[field]; + } + } + + /** + * Construct a Strategy that parses a Text field + * + * @param field The Calendar field + * @param definingCalendar The calendar to obtain the short and long values + * @return a TextStrategy for the field and Locale + */ + private Strategy getLocaleSpecificStrategy(final int field, final Calendar definingCalendar) { + final ConcurrentMap cache = getCache(field); + Strategy strategy = cache.get(locale); + if (strategy == null) { + strategy = field == Calendar.ZONE_OFFSET ? new TimeZoneStrategy(locale) : new CaseInsensitiveTextStrategy(field, definingCalendar, locale); + final Strategy inCache = cache.putIfAbsent(locale, strategy); + if (inCache != null) { + return inCache; + } + } + return strategy; + } + + /** + * A strategy that copies the static or quoted field in the parsing pattern + */ + private static class CopyQuotedStrategy extends Strategy { + + final private String formatField; + + /** + * Construct a Strategy that ensures the formatField has literal text + * + * @param formatField The literal text to match + */ + CopyQuotedStrategy(final String formatField) { + this.formatField = formatField; + } + + @Override + boolean isNumber() { + return false; + } + + @Override + boolean parse(final FastDateParser parser, final Calendar calendar, final String source, final ParsePosition pos, final int maxWidth) { + for (int idx = 0; idx < formatField.length(); ++idx) { + final int sIdx = idx + pos.getIndex(); + if (sIdx == source.length()) { + pos.setErrorIndex(sIdx); + return false; + } + if (formatField.charAt(idx) != source.charAt(sIdx)) { + pos.setErrorIndex(sIdx); + return false; + } + } + pos.setIndex(formatField.length() + pos.getIndex()); + return true; + } + } + + /** + * A strategy that handles a text field in the parsing pattern + */ + private static class CaseInsensitiveTextStrategy extends PatternStrategy { + private final int field; + final Locale locale; + private final Map lKeyValues; + + /** + * Construct a Strategy that parses a Text field + * + * @param field The Calendar field + * @param definingCalendar The Calendar to use + * @param locale The Locale to use + */ + CaseInsensitiveTextStrategy(final int field, final Calendar definingCalendar, final Locale locale) { + this.field = field; + this.locale = locale; + + final StringBuilder regex = new StringBuilder(); + regex.append("((?iu)"); + lKeyValues = appendDisplayNames(definingCalendar, locale, field, regex); + regex.setLength(regex.length() - 1); + regex.append(")"); + createPattern(regex); + } + + @Override + void setCalendar(final FastDateParser parser, final Calendar cal, final String value) { + final Integer iVal = lKeyValues.get(value.toLowerCase(locale)); + cal.set(field, iVal); + } + } + + /** + * A strategy that handles a number field in the parsing pattern + */ + private static class NumberStrategy extends Strategy { + private final int field; + + /** + * Construct a Strategy that parses a Number field + * + * @param field The Calendar field + */ + NumberStrategy(final int field) { + this.field = field; + } + + @Override + boolean isNumber() { + return true; + } + + @Override + boolean parse(final FastDateParser parser, final Calendar calendar, final String source, final ParsePosition pos, final int maxWidth) { + int idx = pos.getIndex(); + int last = source.length(); + + if (maxWidth == 0) { + // if no maxWidth, strip leading white space + for (; idx < last; ++idx) { + final char c = source.charAt(idx); + if (!Character.isWhitespace(c)) { + break; + } + } + pos.setIndex(idx); + } else { + final int end = idx + maxWidth; + if (last > end) { + last = end; + } + } + + for (; idx < last; ++idx) { + final char c = source.charAt(idx); + if (!Character.isDigit(c)) { + break; + } + } + + if (pos.getIndex() == idx) { + pos.setErrorIndex(idx); + return false; + } + + final int value = Integer.parseInt(source.substring(pos.getIndex(), idx)); + pos.setIndex(idx); + + calendar.set(field, modify(parser, value)); + return true; + } + + /** + * Make any modifications to parsed integer + * + * @param parser The parser + * @param iValue The parsed integer + * @return The modified value + */ + int modify(final FastDateParser parser, final int iValue) { + return iValue; + } + + } + + private static final Strategy ABBREVIATED_YEAR_STRATEGY = new NumberStrategy(Calendar.YEAR) { + @Override + int modify(final FastDateParser parser, final int iValue) { + return iValue < 100 ? parser.adjustYear(iValue) : iValue; + } + }; + + /** + * A strategy that handles a timezone field in the parsing pattern + */ + static class TimeZoneStrategy extends PatternStrategy { + private static final String RFC_822_TIME_ZONE = "[+-]\\d{4}"; + private static final String UTC_TIME_ZONE_WITH_OFFSET = "[+-]\\d{2}:\\d{2}"; + private static final String GMT_OPTION = "GMT[+-]\\d{1,2}:\\d{2}"; + + private final Locale locale; + private final Map tzNames = new HashMap<>(); + + private static class TzInfo { + TimeZone zone; + int dstOffset; + + TzInfo(final TimeZone tz, final boolean useDst) { + zone = tz; + dstOffset = useDst ? tz.getDSTSavings() : 0; + } + } + + /** + * Index of zone id + */ + private static final int ID = 0; + + /** + * Construct a Strategy that parses a TimeZone + * + * @param locale The Locale + */ + TimeZoneStrategy(final Locale locale) { + this.locale = locale; + + final StringBuilder sb = new StringBuilder(); + sb.append("((?iu)" + RFC_822_TIME_ZONE + "|" + UTC_TIME_ZONE_WITH_OFFSET + "|" + GMT_OPTION); + + final Set sorted = new TreeSet<>(LONGER_FIRST_LOWERCASE); + + final String[][] zones = DateFormatSymbols.getInstance(locale).getZoneStrings(); + for (final String[] zoneNames : zones) { + // offset 0 is the time zone ID and is not localized + final String tzId = zoneNames[ID]; + if ("GMT".equalsIgnoreCase(tzId)) { + continue; + } + final TimeZone tz = TimeZone.getTimeZone(tzId); + // offset 1 is long standard name + // offset 2 is short standard name + final TzInfo standard = new TzInfo(tz, false); + TzInfo tzInfo = standard; + for (int i = 1; i < zoneNames.length; ++i) { + switch (i) { + case 3: // offset 3 is long daylight savings (or summertime) name + // offset 4 is the short summertime name + tzInfo = new TzInfo(tz, true); + break; + case 5: // offset 5 starts additional names, probably standard time + tzInfo = standard; + break; + } + if (zoneNames[i] != null) { + final String key = zoneNames[i].toLowerCase(locale); + // ignore the data associated with duplicates supplied in + // the additional names + if (sorted.add(key)) { + tzNames.put(key, tzInfo); + } + } + } + } + // order the regex alternatives with longer strings first, greedy + // match will ensure longest string will be consumed + for (final String zoneName : sorted) { + simpleQuote(sb.append('|'), zoneName); + } + sb.append(")"); + createPattern(sb); + } + + @Override + void setCalendar(final FastDateParser parser, final Calendar cal, final String value) { + if (value.charAt(0) == '+' || value.charAt(0) == '-') { + final TimeZone tz = TimeZone.getTimeZone("GMT" + value); + cal.setTimeZone(tz); + } else if (value.regionMatches(true, 0, "GMT", 0, 3)) { + final TimeZone tz = TimeZone.getTimeZone(value.toUpperCase()); + cal.setTimeZone(tz); + } else { + final TzInfo tzInfo = tzNames.get(value.toLowerCase(locale)); + cal.set(Calendar.DST_OFFSET, tzInfo.dstOffset); + //issue#I1AXIN@Gitee +// cal.set(Calendar.ZONE_OFFSET, tzInfo.zone.getRawOffset()); + cal.set(Calendar.ZONE_OFFSET, parser.getTimeZone().getRawOffset()); + } + } + } + + private static class ISO8601TimeZoneStrategy extends PatternStrategy { + // Z, +hh, -hh, +hhmm, -hhmm, +hh:mm or -hh:mm + + /** + * Construct a Strategy that parses a TimeZone + * + * @param pattern The Pattern + */ + ISO8601TimeZoneStrategy(final String pattern) { + createPattern(pattern); + } + + @Override + void setCalendar(final FastDateParser parser, final Calendar cal, final String value) { + if (Objects.equals(value, "Z")) { + cal.setTimeZone(TimeZone.getTimeZone("UTC")); + } else { + cal.setTimeZone(TimeZone.getTimeZone("GMT" + value)); + } + } + + private static final Strategy ISO_8601_1_STRATEGY = new ISO8601TimeZoneStrategy("(Z|(?:[+-]\\d{2}))"); + private static final Strategy ISO_8601_2_STRATEGY = new ISO8601TimeZoneStrategy("(Z|(?:[+-]\\d{2}\\d{2}))"); + private static final Strategy ISO_8601_3_STRATEGY = new ISO8601TimeZoneStrategy("(Z|(?:[+-]\\d{2}(?::)\\d{2}))"); + + /** + * Factory method for ISO8601TimeZoneStrategies. + * + * @param tokenLen a token indicating the length of the TimeZone String to be formatted. + * @return a ISO8601TimeZoneStrategy that can format TimeZone String of length {@code tokenLen}. If no such strategy exists, an IllegalArgumentException will be thrown. + */ + static Strategy getStrategy(final int tokenLen) { + switch (tokenLen) { + case 1: + return ISO_8601_1_STRATEGY; + case 2: + return ISO_8601_2_STRATEGY; + case 3: + return ISO_8601_3_STRATEGY; + default: + throw new IllegalArgumentException("invalid number of X"); + } + } + } + + private static final Strategy NUMBER_MONTH_STRATEGY = new NumberStrategy(Calendar.MONTH) { + @Override + int modify(final FastDateParser parser, final int iValue) { + return iValue - 1; + } + }; + private static final Strategy LITERAL_YEAR_STRATEGY = new NumberStrategy(Calendar.YEAR); + private static final Strategy WEEK_OF_YEAR_STRATEGY = new NumberStrategy(Calendar.WEEK_OF_YEAR); + private static final Strategy WEEK_OF_MONTH_STRATEGY = new NumberStrategy(Calendar.WEEK_OF_MONTH); + private static final Strategy DAY_OF_YEAR_STRATEGY = new NumberStrategy(Calendar.DAY_OF_YEAR); + private static final Strategy DAY_OF_MONTH_STRATEGY = new NumberStrategy(Calendar.DAY_OF_MONTH); + private static final Strategy DAY_OF_WEEK_STRATEGY = new NumberStrategy(Calendar.DAY_OF_WEEK) { + @Override + int modify(final FastDateParser parser, final int iValue) { + return iValue != 7 ? iValue + 1 : Calendar.SUNDAY; + } + }; + private static final Strategy DAY_OF_WEEK_IN_MONTH_STRATEGY = new NumberStrategy(Calendar.DAY_OF_WEEK_IN_MONTH); + private static final Strategy HOUR_OF_DAY_STRATEGY = new NumberStrategy(Calendar.HOUR_OF_DAY); + private static final Strategy HOUR24_OF_DAY_STRATEGY = new NumberStrategy(Calendar.HOUR_OF_DAY) { + @Override + int modify(final FastDateParser parser, final int iValue) { + return iValue == 24 ? 0 : iValue; + } + }; + private static final Strategy HOUR12_STRATEGY = new NumberStrategy(Calendar.HOUR) { + @Override + int modify(final FastDateParser parser, final int iValue) { + return iValue == 12 ? 0 : iValue; + } + }; + private static final Strategy HOUR_STRATEGY = new NumberStrategy(Calendar.HOUR); + private static final Strategy MINUTE_STRATEGY = new NumberStrategy(Calendar.MINUTE); + private static final Strategy SECOND_STRATEGY = new NumberStrategy(Calendar.SECOND); + private static final Strategy MILLISECOND_STRATEGY = new NumberStrategy(Calendar.MILLISECOND); +} diff --git a/src/main/java/cn/hutool/core/date/format/FastDatePrinter.java b/src/main/java/cn/hutool/core/date/format/FastDatePrinter.java new file mode 100644 index 0000000..fb5c270 --- /dev/null +++ b/src/main/java/cn/hutool/core/date/format/FastDatePrinter.java @@ -0,0 +1,1323 @@ +package cn.hutool.core.date.format; + +import cn.hutool.core.date.DateException; +import cn.hutool.core.map.SafeConcurrentHashMap; + +import java.io.IOException; +import java.io.ObjectInputStream; +import java.text.DateFormatSymbols; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Date; +import java.util.List; +import java.util.Locale; +import java.util.TimeZone; +import java.util.concurrent.ConcurrentMap; + +/** + * {@link java.text.SimpleDateFormat} 的线程安全版本,用于将 {@link Date} 格式化输出
+ * Thanks to Apache Commons Lang 3.5 + * + * @see FastDateParser + */ +public class FastDatePrinter extends AbstractDateBasic implements DatePrinter { + private static final long serialVersionUID = -6305750172255764887L; + + /** 规则列表. */ + private transient Rule[] rules; + /** 估算最大长度. */ + private transient int mMaxLengthEstimate; + + // Constructor + // ----------------------------------------------------------------------- + /** + * 构造,内部使用
+ * + * @param pattern 使用{@link java.text.SimpleDateFormat} 相同的日期格式 + * @param timeZone 非空时区{@link TimeZone} + * @param locale 非空{@link Locale} 日期地理位置 + */ + public FastDatePrinter(String pattern, TimeZone timeZone, Locale locale) { + super(pattern, timeZone, locale); + init(); + } + + /** + * 初始化 + */ + private void init() { + final List rulesList = parsePattern(); + rules = rulesList.toArray(new Rule[0]); + + int len = 0; + for (int i = rules.length; --i >= 0;) { + len += rules[i].estimateLength(); + } + + mMaxLengthEstimate = len; + } + + // Parse the pattern + // ----------------------------------------------------------------------- + /** + *

+ * Returns a list of Rules given a pattern. + *

+ * + * @return a {@code List} of Rule objects + * @throws IllegalArgumentException if pattern is invalid + */ + protected List parsePattern() { + final DateFormatSymbols symbols = new DateFormatSymbols(locale); + final List rules = new ArrayList<>(); + + final String[] ERAs = symbols.getEras(); + final String[] months = symbols.getMonths(); + final String[] shortMonths = symbols.getShortMonths(); + final String[] weekdays = symbols.getWeekdays(); + final String[] shortWeekdays = symbols.getShortWeekdays(); + final String[] AmPmStrings = symbols.getAmPmStrings(); + + final int length = pattern.length(); + final int[] indexRef = new int[1]; + + for (int i = 0; i < length; i++) { + indexRef[0] = i; + final String token = parseToken(pattern, indexRef); + i = indexRef[0]; + + final int tokenLen = token.length(); + if (tokenLen == 0) { + break; + } + + Rule rule; + final char c = token.charAt(0); + + switch (c) { + case 'G': // era designator (text) + rule = new TextField(Calendar.ERA, ERAs); + break; + case 'y': // year (number) + case 'Y': // week year + if (tokenLen == 2) { + rule = TwoDigitYearField.INSTANCE; + } else { + rule = selectNumberRule(Calendar.YEAR, Math.max(tokenLen, 4)); + } + if (c == 'Y') { + rule = new WeekYear((NumberRule) rule); + } + break; + case 'M': // month in year (text and number) + if (tokenLen >= 4) { + rule = new TextField(Calendar.MONTH, months); + } else if (tokenLen == 3) { + rule = new TextField(Calendar.MONTH, shortMonths); + } else if (tokenLen == 2) { + rule = TwoDigitMonthField.INSTANCE; + } else { + rule = UnpaddedMonthField.INSTANCE; + } + break; + case 'd': // day in month (number) + rule = selectNumberRule(Calendar.DAY_OF_MONTH, tokenLen); + break; + case 'h': // hour in am/pm (number, 1..12) + rule = new TwelveHourField(selectNumberRule(Calendar.HOUR, tokenLen)); + break; + case 'H': // hour in day (number, 0..23) + rule = selectNumberRule(Calendar.HOUR_OF_DAY, tokenLen); + break; + case 'm': // minute in hour (number) + rule = selectNumberRule(Calendar.MINUTE, tokenLen); + break; + case 's': // second in minute (number) + rule = selectNumberRule(Calendar.SECOND, tokenLen); + break; + case 'S': // millisecond (number) + rule = selectNumberRule(Calendar.MILLISECOND, tokenLen); + break; + case 'E': // day in week (text) + rule = new TextField(Calendar.DAY_OF_WEEK, tokenLen < 4 ? shortWeekdays : weekdays); + break; + case 'u': // day in week (number) + rule = new DayInWeekField(selectNumberRule(Calendar.DAY_OF_WEEK, tokenLen)); + break; + case 'D': // day in year (number) + rule = selectNumberRule(Calendar.DAY_OF_YEAR, tokenLen); + break; + case 'F': // day of week in month (number) + rule = selectNumberRule(Calendar.DAY_OF_WEEK_IN_MONTH, tokenLen); + break; + case 'w': // week in year (number) + rule = selectNumberRule(Calendar.WEEK_OF_YEAR, tokenLen); + break; + case 'W': // week in month (number) + rule = selectNumberRule(Calendar.WEEK_OF_MONTH, tokenLen); + break; + case 'a': // am/pm marker (text) + rule = new TextField(Calendar.AM_PM, AmPmStrings); + break; + case 'k': // hour in day (1..24) + rule = new TwentyFourHourField(selectNumberRule(Calendar.HOUR_OF_DAY, tokenLen)); + break; + case 'K': // hour in am/pm (0..11) + rule = selectNumberRule(Calendar.HOUR, tokenLen); + break; + case 'X': // ISO 8601 + rule = Iso8601_Rule.getRule(tokenLen); + break; + case 'z': // time zone (text) + if (tokenLen >= 4) { + rule = new TimeZoneNameRule(timeZone, locale, TimeZone.LONG); + } else { + rule = new TimeZoneNameRule(timeZone, locale, TimeZone.SHORT); + } + break; + case 'Z': // time zone (value) + if (tokenLen == 1) { + rule = TimeZoneNumberRule.INSTANCE_NO_COLON; + } else if (tokenLen == 2) { + rule = Iso8601_Rule.ISO8601_HOURS_COLON_MINUTES; + } else { + rule = TimeZoneNumberRule.INSTANCE_COLON; + } + break; + case '\'': // literal text + final String sub = token.substring(1); + if (sub.length() == 1) { + rule = new CharacterLiteral(sub.charAt(0)); + } else { + rule = new StringLiteral(sub); + } + break; + default: + throw new IllegalArgumentException("Illegal pattern component: " + token); + } + + rules.add(rule); + } + + return rules; + } + + /** + *

+ * Performs the parsing of tokens. + *

+ * + * @param pattern the pattern + * @param indexRef index references + * @return parsed token + */ + protected String parseToken(String pattern, int[] indexRef) { + final StringBuilder buf = new StringBuilder(); + + int i = indexRef[0]; + final int length = pattern.length(); + + char c = pattern.charAt(i); + if (c >= 'A' && c <= 'Z' || c >= 'a' && c <= 'z') { + // Scan a run of the same character, which indicates a time + // pattern. + buf.append(c); + + while (i + 1 < length) { + final char peek = pattern.charAt(i + 1); + if (peek == c) { + buf.append(c); + i++; + } else { + break; + } + } + } else { + // This will identify token as text. + buf.append('\''); + + boolean inLiteral = false; + + for (; i < length; i++) { + c = pattern.charAt(i); + + if (c == '\'') { + if (i + 1 < length && pattern.charAt(i + 1) == '\'') { + // '' is treated as escaped ' + i++; + buf.append(c); + } else { + inLiteral = !inLiteral; + } + } else if (!inLiteral && (c >= 'A' && c <= 'Z' || c >= 'a' && c <= 'z')) { + i--; + break; + } else { + buf.append(c); + } + } + } + + indexRef[0] = i; + return buf.toString(); + } + + /** + *

+ * Gets an appropriate rule for the padding required. + *

+ * + * @param field the field to get a rule for + * @param padding the padding required + * @return a new rule with the correct padding + */ + protected NumberRule selectNumberRule(int field, int padding) { + switch (padding) { + case 1: + return new UnpaddedNumberField(field); + case 2: + return new TwoDigitNumberField(field); + default: + return new PaddedNumberField(field, padding); + } + } + + // Format methods + // ----------------------------------------------------------------------- + + /** + *

+ * Formats a {@code Date}, {@code Calendar} or {@code Long} (milliseconds) object. + *

+ * + * @param obj the object to format + * @return The formatted value. + */ + String format(Object obj) { + if (obj instanceof Date) { + return format((Date) obj); + } else if (obj instanceof Calendar) { + return format((Calendar) obj); + } else if (obj instanceof Long) { + return format(((Long) obj).longValue()); + } else { + throw new IllegalArgumentException("Unknown class: " + (obj == null ? "" : obj.getClass().getName())); + } + } + + @Override + public String format(long millis) { + final Calendar c = Calendar.getInstance(timeZone, locale); + c.setTimeInMillis(millis); + return applyRulesToString(c); + } + + @Override + public String format(Date date) { + final Calendar c = Calendar.getInstance(timeZone, locale); + c.setTime(date); + return applyRulesToString(c); + } + + @Override + public String format(Calendar calendar) { + return format(calendar, new StringBuilder(mMaxLengthEstimate)).toString(); + } + + @Override + public B format(long millis, B buf) { + final Calendar c = Calendar.getInstance(timeZone, locale); + c.setTimeInMillis(millis); + return applyRules(c, buf); + } + + @Override + public B format(Date date, B buf) { + final Calendar c = Calendar.getInstance(timeZone, locale); + c.setTime(date); + return applyRules(c, buf); + } + + @Override + public B format(Calendar calendar, B buf) { + // do not pass in calendar directly, this will cause TimeZone of FastDatePrinter to be ignored + if (!calendar.getTimeZone().equals(timeZone)) { + calendar = (Calendar) calendar.clone(); + calendar.setTimeZone(timeZone); + } + return applyRules(calendar, buf); + } + + /** + * Creates a String representation of the given Calendar by applying the rules of this printer to it. + * + * @param c the Calender to apply the rules to. + * @return a String representation of the given Calendar. + */ + private String applyRulesToString(Calendar c) { + return applyRules(c, new StringBuilder(mMaxLengthEstimate)).toString(); + } + + /** + *

+ * Performs the formatting by applying the rules to the specified calendar. + *

+ * + * @param calendar the calendar to format + * @param buf the buffer to format into + * @param the Appendable class type, usually StringBuilder or StringBuffer. + * @return the specified string buffer + */ + private B applyRules(Calendar calendar, B buf) { + try { + for (final Rule rule : this.rules) { + rule.appendTo(buf, calendar); + } + } catch (final IOException e) { + throw new DateException(e); + } + return buf; + } + + /** + *估算生成的日期字符串长度
+ * 实际生成的字符串长度小于或等于此值 + * + * @return 日期字符串长度 + */ + public int getMaxLengthEstimate() { + return mMaxLengthEstimate; + } + + // Serializing + // ----------------------------------------------------------------------- + /** + * Create the object after serialization. This implementation reinitializes the transient properties. + * + * @param in ObjectInputStream from which the object is being deserialized. + * @throws IOException if there is an IO issue. + * @throws ClassNotFoundException if a class cannot be found. + */ + private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { + in.defaultReadObject(); + init(); + } + + /** + * Appends two digits to the given buffer. + * + * @param buffer the buffer to append to. + * @param value the value to append digits from. + */ + private static void appendDigits(Appendable buffer, int value) throws IOException { + buffer.append((char) (value / 10 + '0')); + buffer.append((char) (value % 10 + '0')); + } + + private static final int MAX_DIGITS = 10; // log10(Integer.MAX_VALUE) ~= 9.3 + + /** + * Appends all digits to the given buffer. + * + * @param buffer the buffer to append to. + * @param value the value to append digits from. + */ + private static void appendFullDigits(Appendable buffer, int value, int minFieldWidth) throws IOException { + // specialized paths for 1 to 4 digits -> avoid the memory allocation from the temporary work array + // see LANG-1248 + if (value < 10000) { + // less memory allocation path works for four digits or less + + int nDigits = 4; + if (value < 1000) { + --nDigits; + if (value < 100) { + --nDigits; + if (value < 10) { + --nDigits; + } + } + } + // left zero pad + for (int i = minFieldWidth - nDigits; i > 0; --i) { + buffer.append('0'); + } + + switch (nDigits) { + case 4: + buffer.append((char) (value / 1000 + '0')); + value %= 1000; + case 3: + if (value >= 100) { + buffer.append((char) (value / 100 + '0')); + value %= 100; + } else { + buffer.append('0'); + } + case 2: + if (value >= 10) { + buffer.append((char) (value / 10 + '0')); + value %= 10; + } else { + buffer.append('0'); + } + case 1: + buffer.append((char) (value + '0')); + } + } else { + // more memory allocation path works for any digits + + // build up decimal representation in reverse + final char[] work = new char[MAX_DIGITS]; + int digit = 0; + while (value != 0) { + work[digit++] = (char) (value % 10 + '0'); + value = value / 10; + } + + // pad with zeros + while (digit < minFieldWidth) { + buffer.append('0'); + --minFieldWidth; + } + + // reverse + while (--digit >= 0) { + buffer.append(work[digit]); + } + } + } + + // Rules + // ----------------------------------------------------------------------- + /** + * 规则 + */ + private interface Rule { + /** + * Returns the estimated length of the result. + * + * @return the estimated length + */ + int estimateLength(); + + /** + * Appends the value of the specified calendar to the output buffer based on the rule implementation. + * + * @param buf the output buffer + * @param calendar calendar to be appended + * @throws IOException if an I/O error occurs + */ + void appendTo(Appendable buf, Calendar calendar) throws IOException; + } + + /** + *

+ * Inner class defining a numeric rule. + *

+ */ + private interface NumberRule extends Rule { + /** + * Appends the specified value to the output buffer based on the rule implementation. + * + * @param buffer the output buffer + * @param value the value to be appended + * @throws IOException if an I/O error occurs + */ + void appendTo(Appendable buffer, int value) throws IOException; + } + + /** + *

+ * Inner class to output a constant single character. + *

+ */ + private static class CharacterLiteral implements Rule { + private final char mValue; + + /** + * Constructs a new instance of {@code CharacterLiteral} to hold the specified value. + * + * @param value the character literal + */ + CharacterLiteral(final char value) { + mValue = value; + } + + @Override + public int estimateLength() { + return 1; + } + + @Override + public void appendTo(final Appendable buffer, final Calendar calendar) throws IOException { + buffer.append(mValue); + } + } + + /** + *

+ * Inner class to output a constant string. + *

+ */ + private static class StringLiteral implements Rule { + private final String mValue; + + /** + * Constructs a new instance of {@code StringLiteral} to hold the specified value. + * + * @param value the string literal + */ + StringLiteral(String value) { + mValue = value; + } + + /** + * {@inheritDoc} + */ + @Override + public int estimateLength() { + return mValue.length(); + } + + /** + * {@inheritDoc} + */ + @Override + public void appendTo(Appendable buffer, Calendar calendar) throws IOException { + buffer.append(mValue); + } + } + + /** + *

+ * Inner class to output one of a set of values. + *

+ */ + private static class TextField implements Rule { + private final int mField; + private final String[] mValues; + + /** + * Constructs an instance of {@code TextField} with the specified field and values. + * + * @param field the field + * @param values the field values + */ + TextField(int field, String[] values) { + mField = field; + mValues = values; + } + + /** + * {@inheritDoc} + */ + @Override + public int estimateLength() { + int max = 0; + for (int i = mValues.length; --i >= 0;) { + final int len = mValues[i].length(); + if (len > max) { + max = len; + } + } + return max; + } + + /** + * {@inheritDoc} + */ + @Override + public void appendTo(Appendable buffer, Calendar calendar) throws IOException { + buffer.append(mValues[calendar.get(mField)]); + } + } + + /** + *

+ * Inner class to output an unpadded number. + *

+ */ + private static class UnpaddedNumberField implements NumberRule { + private final int mField; + + /** + * Constructs an instance of {@code UnpadedNumberField} with the specified field. + * + * @param field the field + */ + UnpaddedNumberField(int field) { + mField = field; + } + + /** + * {@inheritDoc} + */ + @Override + public int estimateLength() { + return 4; + } + + /** + * {@inheritDoc} + */ + @Override + public void appendTo(Appendable buffer, Calendar calendar) throws IOException { + appendTo(buffer, calendar.get(mField)); + } + + /** + * {@inheritDoc} + */ + @Override + public final void appendTo(Appendable buffer, int value) throws IOException { + if (value < 10) { + buffer.append((char) (value + '0')); + } else if (value < 100) { + appendDigits(buffer, value); + } else { + appendFullDigits(buffer, value, 1); + } + } + } + + /** + *

+ * Inner class to output an unpadded month. + *

+ */ + private static class UnpaddedMonthField implements NumberRule { + static final UnpaddedMonthField INSTANCE = new UnpaddedMonthField(); + + /** + * Constructs an instance of {@code UnpaddedMonthField}. + * + */ + UnpaddedMonthField() { + } + + /** + * {@inheritDoc} + */ + @Override + public int estimateLength() { + return 2; + } + + /** + * {@inheritDoc} + */ + @Override + public void appendTo(Appendable buffer, Calendar calendar) throws IOException { + appendTo(buffer, calendar.get(Calendar.MONTH) + 1); + } + + /** + * {@inheritDoc} + */ + @Override + public final void appendTo(Appendable buffer, int value) throws IOException { + if (value < 10) { + buffer.append((char) (value + '0')); + } else { + appendDigits(buffer, value); + } + } + } + + /** + *

+ * Inner class to output a padded number. + *

+ */ + private static class PaddedNumberField implements NumberRule { + private final int mField; + private final int mSize; + + /** + * Constructs an instance of {@code PaddedNumberField}. + * + * @param field the field + * @param size size of the output field + */ + PaddedNumberField(int field, int size) { + if (size < 3) { + // Should use UnpaddedNumberField or TwoDigitNumberField. + throw new IllegalArgumentException(); + } + mField = field; + mSize = size; + } + + /** + * {@inheritDoc} + */ + @Override + public int estimateLength() { + return mSize; + } + + /** + * {@inheritDoc} + */ + @Override + public void appendTo(Appendable buffer, Calendar calendar) throws IOException { + appendTo(buffer, calendar.get(mField)); + } + + /** + * {@inheritDoc} + */ + @Override + public final void appendTo(Appendable buffer, int value) throws IOException { + appendFullDigits(buffer, value, mSize); + } + } + + /** + *

+ * Inner class to output a two digit number. + *

+ */ + private static class TwoDigitNumberField implements NumberRule { + private final int mField; + + /** + * Constructs an instance of {@code TwoDigitNumberField} with the specified field. + * + * @param field the field + */ + TwoDigitNumberField(int field) { + mField = field; + } + + /** + * {@inheritDoc} + */ + @Override + public int estimateLength() { + return 2; + } + + /** + * {@inheritDoc} + */ + @Override + public void appendTo(Appendable buffer, Calendar calendar) throws IOException { + appendTo(buffer, calendar.get(mField)); + } + + /** + * {@inheritDoc} + */ + @Override + public final void appendTo(Appendable buffer, int value) throws IOException { + if (value < 100) { + appendDigits(buffer, value); + } else { + appendFullDigits(buffer, value, 2); + } + } + } + + /** + *

+ * Inner class to output a two digit year. + *

+ */ + private static class TwoDigitYearField implements NumberRule { + static final TwoDigitYearField INSTANCE = new TwoDigitYearField(); + + /** + * Constructs an instance of {@code TwoDigitYearField}. + */ + TwoDigitYearField() { + } + + /** + * {@inheritDoc} + */ + @Override + public int estimateLength() { + return 2; + } + + /** + * {@inheritDoc} + */ + @Override + public void appendTo(Appendable buffer, Calendar calendar) throws IOException { + appendTo(buffer, calendar.get(Calendar.YEAR) % 100); + } + + /** + * {@inheritDoc} + */ + @Override + public final void appendTo(Appendable buffer, int value) throws IOException { + appendDigits(buffer, value); + } + } + + /** + *

+ * Inner class to output a two digit month. + *

+ */ + private static class TwoDigitMonthField implements NumberRule { + static final TwoDigitMonthField INSTANCE = new TwoDigitMonthField(); + + /** + * Constructs an instance of {@code TwoDigitMonthField}. + */ + TwoDigitMonthField() { + } + + /** + * {@inheritDoc} + */ + @Override + public int estimateLength() { + return 2; + } + + /** + * {@inheritDoc} + */ + @Override + public void appendTo(Appendable buffer, Calendar calendar) throws IOException { + appendTo(buffer, calendar.get(Calendar.MONTH) + 1); + } + + /** + * {@inheritDoc} + */ + @Override + public final void appendTo(Appendable buffer, int value) throws IOException { + appendDigits(buffer, value); + } + } + + /** + *

+ * Inner class to output the twelve hour field. + *

+ */ + private static class TwelveHourField implements NumberRule { + private final NumberRule mRule; + + /** + * Constructs an instance of {@code TwelveHourField} with the specified {@code NumberRule}. + * + * @param rule the rule + */ + TwelveHourField(final NumberRule rule) { + mRule = rule; + } + + /** + * {@inheritDoc} + */ + @Override + public int estimateLength() { + return mRule.estimateLength(); + } + + /** + * {@inheritDoc} + */ + @Override + public void appendTo(Appendable buffer, Calendar calendar) throws IOException { + int value = calendar.get(Calendar.HOUR); + if (value == 0) { + value = calendar.getLeastMaximum(Calendar.HOUR) + 1; + } + mRule.appendTo(buffer, value); + } + + /** + * {@inheritDoc} + */ + @Override + public void appendTo(Appendable buffer, int value) throws IOException { + mRule.appendTo(buffer, value); + } + } + + /** + *

+ * Inner class to output the twenty four hour field. + *

+ */ + private static class TwentyFourHourField implements NumberRule { + private final NumberRule mRule; + + /** + * Constructs an instance of {@code TwentyFourHourField} with the specified {@code NumberRule}. + * + * @param rule the rule + */ + TwentyFourHourField(NumberRule rule) { + mRule = rule; + } + + /** + * {@inheritDoc} + */ + @Override + public int estimateLength() { + return mRule.estimateLength(); + } + + /** + * {@inheritDoc} + */ + @Override + public void appendTo(Appendable buffer, Calendar calendar) throws IOException { + int value = calendar.get(Calendar.HOUR_OF_DAY); + if (value == 0) { + value = calendar.getMaximum(Calendar.HOUR_OF_DAY) + 1; + } + mRule.appendTo(buffer, value); + } + + /** + * {@inheritDoc} + */ + @Override + public void appendTo(Appendable buffer, int value) throws IOException { + mRule.appendTo(buffer, value); + } + } + + /** + *

+ * Inner class to output the numeric day in week. + *

+ */ + private static class DayInWeekField implements NumberRule { + private final NumberRule mRule; + + DayInWeekField(NumberRule rule) { + mRule = rule; + } + + @Override + public int estimateLength() { + return mRule.estimateLength(); + } + + @Override + public void appendTo(Appendable buffer, Calendar calendar) throws IOException { + final int value = calendar.get(Calendar.DAY_OF_WEEK); + mRule.appendTo(buffer, value != Calendar.SUNDAY ? value - 1 : 7); + } + + @Override + public void appendTo(Appendable buffer, int value) throws IOException { + mRule.appendTo(buffer, value); + } + } + + /** + *

+ * Inner class to output the numeric day in week. + *

+ */ + private static class WeekYear implements NumberRule { + private final NumberRule mRule; + + WeekYear(final NumberRule rule) { + mRule = rule; + } + + @Override + public int estimateLength() { + return mRule.estimateLength(); + } + + @Override + public void appendTo(Appendable buffer, Calendar calendar) throws IOException { + mRule.appendTo(buffer, calendar.getWeekYear()); + } + + @Override + public void appendTo(Appendable buffer, int value) throws IOException { + mRule.appendTo(buffer, value); + } + } + + // ----------------------------------------------------------------------- + + private static final ConcurrentMap C_TIME_ZONE_DISPLAY_CACHE = new SafeConcurrentHashMap<>(7); + + /** + *

+ * Gets the time zone display name, using a cache for performance. + *

+ * + * @param tz the zone to query + * @param daylight true if daylight savings + * @param style the style to use {@code TimeZone.LONG} or {@code TimeZone.SHORT} + * @param locale the locale to use + * @return the textual name of the time zone + */ + static String getTimeZoneDisplay(TimeZone tz, boolean daylight, int style, Locale locale) { + final TimeZoneDisplayKey key = new TimeZoneDisplayKey(tz, daylight, style, locale); + String value = C_TIME_ZONE_DISPLAY_CACHE.get(key); + if (value == null) { + // This is a very slow call, so cache the results. + value = tz.getDisplayName(daylight, style, locale); + final String prior = C_TIME_ZONE_DISPLAY_CACHE.putIfAbsent(key, value); + if (prior != null) { + value = prior; + } + } + return value; + } + + /** + *

+ * Inner class to output a time zone name. + *

+ */ + private static class TimeZoneNameRule implements Rule { + private final Locale mLocale; + private final int mStyle; + private final String mStandard; + private final String mDaylight; + + /** + * Constructs an instance of {@code TimeZoneNameRule} with the specified properties. + * + * @param timeZone the time zone + * @param locale the locale + * @param style the style + */ + TimeZoneNameRule(TimeZone timeZone, Locale locale, int style) { + mLocale = locale; + mStyle = style; + + mStandard = getTimeZoneDisplay(timeZone, false, style, locale); + mDaylight = getTimeZoneDisplay(timeZone, true, style, locale); + } + + /** + * {@inheritDoc} + */ + @Override + public int estimateLength() { + // We have no access to the Calendar object that will be passed to + // appendTo so base estimate on the TimeZone passed to the + // constructor + return Math.max(mStandard.length(), mDaylight.length()); + } + + /** + * {@inheritDoc} + */ + @Override + public void appendTo(Appendable buffer, Calendar calendar) throws IOException { + final TimeZone zone = calendar.getTimeZone(); + if (calendar.get(Calendar.DST_OFFSET) != 0) { + buffer.append(getTimeZoneDisplay(zone, true, mStyle, mLocale)); + } else { + buffer.append(getTimeZoneDisplay(zone, false, mStyle, mLocale)); + } + } + } + + /** + *

+ * Inner class to output a time zone as a number {@code +/-HHMM} or {@code +/-HH:MM}. + *

+ */ + private static class TimeZoneNumberRule implements Rule { + static final TimeZoneNumberRule INSTANCE_COLON = new TimeZoneNumberRule(true); + static final TimeZoneNumberRule INSTANCE_NO_COLON = new TimeZoneNumberRule(false); + + final boolean mColon; + + /** + * Constructs an instance of {@code TimeZoneNumberRule} with the specified properties. + * + * @param colon add colon between HH and MM in the output if {@code true} + */ + TimeZoneNumberRule(boolean colon) { + mColon = colon; + } + + /** + * {@inheritDoc} + */ + @Override + public int estimateLength() { + return 5; + } + + /** + * {@inheritDoc} + */ + @Override + public void appendTo(Appendable buffer, Calendar calendar) throws IOException { + + int offset = calendar.get(Calendar.ZONE_OFFSET) + calendar.get(Calendar.DST_OFFSET); + + if (offset < 0) { + buffer.append('-'); + offset = -offset; + } else { + buffer.append('+'); + } + + final int hours = offset / (60 * 60 * 1000); + appendDigits(buffer, hours); + + if (mColon) { + buffer.append(':'); + } + + final int minutes = offset / (60 * 1000) - 60 * hours; + appendDigits(buffer, minutes); + } + } + + /** + *

+ * Inner class to output a time zone as a number {@code +/-HHMM} or {@code +/-HH:MM}. + *

+ */ + private static class Iso8601_Rule implements Rule { + + // Sign TwoDigitHours or Z + static final Iso8601_Rule ISO8601_HOURS = new Iso8601_Rule(3); + // Sign TwoDigitHours Minutes or Z + static final Iso8601_Rule ISO8601_HOURS_MINUTES = new Iso8601_Rule(5); + // Sign TwoDigitHours : Minutes or Z + static final Iso8601_Rule ISO8601_HOURS_COLON_MINUTES = new Iso8601_Rule(6); + + /** + * Factory method for Iso8601_Rules. + * + * @param tokenLen a token indicating the length of the TimeZone String to be formatted. + * @return a Iso8601_Rule that can format TimeZone String of length {@code tokenLen}. If no such rule exists, an IllegalArgumentException will be thrown. + */ + static Iso8601_Rule getRule(int tokenLen) { + switch (tokenLen) { + case 1: + return Iso8601_Rule.ISO8601_HOURS; + case 2: + return Iso8601_Rule.ISO8601_HOURS_MINUTES; + case 3: + return Iso8601_Rule.ISO8601_HOURS_COLON_MINUTES; + default: + throw new IllegalArgumentException("invalid number of X"); + } + } + + final int length; + + /** + * Constructs an instance of {@code Iso8601_Rule} with the specified properties. + * + * @param length The number of characters in output (unless Z is output) + */ + Iso8601_Rule(int length) { + this.length = length; + } + + /** + * {@inheritDoc} + */ + @Override + public int estimateLength() { + return length; + } + + /** + * {@inheritDoc} + */ + @Override + public void appendTo(final Appendable buffer, final Calendar calendar) throws IOException { + int offset = calendar.get(Calendar.ZONE_OFFSET) + calendar.get(Calendar.DST_OFFSET); + if (offset == 0) { + buffer.append("Z"); + return; + } + + if (offset < 0) { + buffer.append('-'); + offset = -offset; + } else { + buffer.append('+'); + } + + final int hours = offset / (60 * 60 * 1000); + appendDigits(buffer, hours); + + if (length < 5) { + return; + } + + if (length == 6) { + buffer.append(':'); + } + + final int minutes = offset / (60 * 1000) - 60 * hours; + appendDigits(buffer, minutes); + } + } + + // ---------------------------------------------------------------------- + /** + *

+ * Inner class that acts as a compound key for time zone names. + *

+ */ + private static class TimeZoneDisplayKey { + private final TimeZone mTimeZone; + private final int mStyle; + private final Locale mLocale; + + /** + * Constructs an instance of {@code TimeZoneDisplayKey} with the specified properties. + * + * @param timeZone the time zone + * @param daylight adjust the style for daylight saving time if {@code true} + * @param style the timezone style + * @param locale the timezone locale + */ + TimeZoneDisplayKey(final TimeZone timeZone, final boolean daylight, final int style, final Locale locale) { + mTimeZone = timeZone; + if (daylight) { + mStyle = style | 0x80000000; + } else { + mStyle = style; + } + mLocale = locale; + } + + /** + * {@inheritDoc} + */ + @Override + public int hashCode() { + return (mStyle * 31 + mLocale.hashCode()) * 31 + mTimeZone.hashCode(); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean equals(final Object obj) { + if (this == obj) { + return true; + } + if (obj instanceof TimeZoneDisplayKey) { + final TimeZoneDisplayKey other = (TimeZoneDisplayKey) obj; + return mTimeZone.equals(other.mTimeZone) && mStyle == other.mStyle && mLocale.equals(other.mLocale); + } + return false; + } + } +} diff --git a/src/main/java/cn/hutool/core/date/format/FormatCache.java b/src/main/java/cn/hutool/core/date/format/FormatCache.java new file mode 100644 index 0000000..ba1804e --- /dev/null +++ b/src/main/java/cn/hutool/core/date/format/FormatCache.java @@ -0,0 +1,175 @@ +package cn.hutool.core.date.format; + +import cn.hutool.core.lang.Assert; +import cn.hutool.core.lang.Tuple; +import cn.hutool.core.map.SafeConcurrentHashMap; + +import java.text.DateFormat; +import java.text.Format; +import java.text.SimpleDateFormat; +import java.util.Locale; +import java.util.TimeZone; +import java.util.concurrent.ConcurrentMap; + +/** + * 日期格式化器缓存
+ * Thanks to Apache Commons Lang 3.5 + * + * @since 2.16.2 + */ +abstract class FormatCache { + + /** + * No date or no time. Used in same parameters as DateFormat.SHORT or DateFormat.LONG + */ + static final int NONE = -1; + + private final ConcurrentMap cInstanceCache = new SafeConcurrentHashMap<>(7); + + private static final ConcurrentMap C_DATE_TIME_INSTANCE_CACHE = new SafeConcurrentHashMap<>(7); + + /** + * 使用默认的pattern、timezone和locale获得缓存中的实例 + * + * @return a date/time formatter + */ + public F getInstance() { + return getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT, null, null); + } + + /** + * 使用 pattern, time zone and locale 获得对应的 格式化器 + * + * @param pattern 非空日期格式,使用与 {@link SimpleDateFormat}相同格式 + * @param timeZone 时区,默认当前时区 + * @param locale 地区,默认使用当前地区 + * @return 格式化器 + * @throws IllegalArgumentException pattern 无效或{@code null} + */ + public F getInstance(final String pattern, TimeZone timeZone, Locale locale) { + Assert.notBlank(pattern, "pattern must not be blank"); + if (timeZone == null) { + timeZone = TimeZone.getDefault(); + } + if (locale == null) { + locale = Locale.getDefault(); + } + final Tuple key = new Tuple(pattern, timeZone, locale); + F format = cInstanceCache.get(key); + if (format == null) { + format = createInstance(pattern, timeZone, locale); + final F previousValue = cInstanceCache.putIfAbsent(key, format); + if (previousValue != null) { + // another thread snuck in and did the same work + // we should return the instance that is in ConcurrentMap + format = previousValue; + } + } + return format; + } + + /** + * 创建格式化器 + * + * @param pattern 非空日期格式,使用与 {@link SimpleDateFormat}相同格式 + * @param timeZone 时区,默认当前时区 + * @param locale 地区,默认使用当前地区 + * @return 格式化器 + * @throws IllegalArgumentException pattern 无效或{@code null} + */ + abstract protected F createInstance(String pattern, TimeZone timeZone, Locale locale); + + /** + *

+ * Gets a date/time formatter instance using the specified style, time zone and locale. + *

+ * + * @param dateStyle date style: FULL, LONG, MEDIUM, or SHORT, null indicates no date in format + * @param timeStyle time style: FULL, LONG, MEDIUM, or SHORT, null indicates no time in format + * @param timeZone optional time zone, overrides time zone of formatted date, null means use default Locale + * @param locale optional locale, overrides system locale + * @return a localized standard date/time formatter + * @throws IllegalArgumentException if the Locale has no date/time pattern defined + */ + // This must remain private, see LANG-884 + F getDateTimeInstance(final Integer dateStyle, final Integer timeStyle, final TimeZone timeZone, Locale locale) { + if (locale == null) { + locale = Locale.getDefault(); + } + final String pattern = getPatternForStyle(dateStyle, timeStyle, locale); + return getInstance(pattern, timeZone, locale); + } + + /** + *

+ * Gets a date formatter instance using the specified style, time zone and locale. + *

+ * + * @param dateStyle date style: FULL, LONG, MEDIUM, or SHORT + * @param timeZone optional time zone, overrides time zone of formatted date, null means use default Locale + * @param locale optional locale, overrides system locale + * @return a localized standard date/time formatter + * @throws IllegalArgumentException if the Locale has no date/time pattern defined + */ + // package protected, for access from FastDateFormat; do not make public or protected + F getDateInstance(final int dateStyle, final TimeZone timeZone, final Locale locale) { + return getDateTimeInstance(dateStyle, null, timeZone, locale); + } + + /** + *

+ * Gets a time formatter instance using the specified style, time zone and locale. + *

+ * + * @param timeStyle time style: FULL, LONG, MEDIUM, or SHORT + * @param timeZone optional time zone, overrides time zone of formatted date, null means use default Locale + * @param locale optional locale, overrides system locale + * @return a localized standard date/time formatter + * @throws IllegalArgumentException if the Locale has no date/time pattern defined + */ + // package protected, for access from FastDateFormat; do not make public or protected + F getTimeInstance(final int timeStyle, final TimeZone timeZone, final Locale locale) { + return getDateTimeInstance(null, timeStyle, timeZone, locale); + } + + /** + *

+ * Gets a date/time format for the specified styles and locale. + *

+ * + * @param dateStyle date style: FULL, LONG, MEDIUM, or SHORT, null indicates no date in format + * @param timeStyle time style: FULL, LONG, MEDIUM, or SHORT, null indicates no time in format + * @param locale The non-null locale of the desired format + * @return a localized standard date/time format + * @throws IllegalArgumentException if the Locale has no date/time pattern defined + */ + // package protected, for access from test code; do not make public or protected + static String getPatternForStyle(final Integer dateStyle, final Integer timeStyle, final Locale locale) { + final Tuple key = new Tuple(dateStyle, timeStyle, locale); + + String pattern = C_DATE_TIME_INSTANCE_CACHE.get(key); + if (pattern == null) { + try { + DateFormat formatter; + if (dateStyle == null) { + formatter = DateFormat.getTimeInstance(timeStyle, locale); + } else if (timeStyle == null) { + formatter = DateFormat.getDateInstance(dateStyle, locale); + } else { + formatter = DateFormat.getDateTimeInstance(dateStyle, timeStyle, locale); + } + pattern = ((SimpleDateFormat) formatter).toPattern(); + final String previous = C_DATE_TIME_INSTANCE_CACHE.putIfAbsent(key, pattern); + if (previous != null) { + // even though it doesn't matter if another thread put the pattern + // it's still good practice to return the String instance that is + // actually in the ConcurrentMap + pattern = previous; + } + } catch (final ClassCastException ex) { + throw new IllegalArgumentException("No date time pattern for locale: " + locale); + } + } + return pattern; + } +} diff --git a/src/main/java/cn/hutool/core/date/format/GlobalCustomFormat.java b/src/main/java/cn/hutool/core/date/format/GlobalCustomFormat.java new file mode 100644 index 0000000..602e335 --- /dev/null +++ b/src/main/java/cn/hutool/core/date/format/GlobalCustomFormat.java @@ -0,0 +1,125 @@ +package cn.hutool.core.date.format; + +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.map.SafeConcurrentHashMap; + +import java.time.temporal.TemporalAccessor; +import java.util.Date; +import java.util.Map; +import java.util.function.Function; + +/** + * 全局自定义格式
+ * 用于定义用户指定的日期格式和输出日期的关系 + * + * @author looly + * @since 5.7.2 + */ +public class GlobalCustomFormat { + + /** + * 格式:秒时间戳(Unix时间戳) + */ + public static final String FORMAT_SECONDS = "#sss"; + /** + * 格式:毫秒时间戳 + */ + public static final String FORMAT_MILLISECONDS = "#SSS"; + + private static final Map> formatterMap; + private static final Map> parserMap; + + static { + formatterMap = new SafeConcurrentHashMap<>(); + parserMap = new SafeConcurrentHashMap<>(); + + // Hutool预设的几种自定义格式 + putFormatter(FORMAT_SECONDS, (date) -> String.valueOf(Math.floorDiv(date.getTime(), 1000))); + putParser(FORMAT_SECONDS, (dateStr) -> DateUtil.date(Math.multiplyExact(Long.parseLong(dateStr.toString()), 1000))); + + putFormatter(FORMAT_MILLISECONDS, (date) -> String.valueOf(date.getTime())); + putParser(FORMAT_MILLISECONDS, (dateStr) -> DateUtil.date(Long.parseLong(dateStr.toString()))); + } + + /** + * 加入日期格式化规则 + * + * @param format 格式 + * @param func 格式化函数 + */ + public static void putFormatter(String format, Function func) { + Assert.notNull(format, "Format must be not null !"); + Assert.notNull(func, "Function must be not null !"); + formatterMap.put(format, func); + } + + /** + * 加入日期解析规则 + * + * @param format 格式 + * @param func 解析函数 + */ + public static void putParser(String format, Function func) { + Assert.notNull(format, "Format must be not null !"); + Assert.notNull(func, "Function must be not null !"); + parserMap.put(format, func); + } + + /** + * 检查指定格式是否为自定义格式 + * + * @param format 格式 + * @return 是否为自定义格式 + */ + public static boolean isCustomFormat(String format) { + return formatterMap.containsKey(format); + } + + /** + * 使用自定义格式格式化日期 + * + * @param date 日期 + * @param format 自定义格式 + * @return 格式化后的日期 + */ + public static String format(Date date, CharSequence format) { + if (null != formatterMap) { + final Function func = formatterMap.get(format); + if (null != func) { + return func.apply(date); + } + } + + return null; + } + + /** + * 使用自定义格式格式化日期 + * + * @param temporalAccessor 日期 + * @param format 自定义格式 + * @return 格式化后的日期 + */ + public static String format(TemporalAccessor temporalAccessor, CharSequence format) { + return format(DateUtil.date(temporalAccessor), format); + } + + /** + * 使用自定义格式解析日期 + * + * @param dateStr 日期字符串 + * @param format 自定义格式 + * @return 格式化后的日期 + */ + public static Date parse(CharSequence dateStr, String format) { + if (null != parserMap) { + final Function func = parserMap.get(format); + if (null != func) { + return func.apply(dateStr); + } + } + + return null; + } +} diff --git a/src/main/java/cn/hutool/core/date/format/package-info.java b/src/main/java/cn/hutool/core/date/format/package-info.java new file mode 100644 index 0000000..13fe578 --- /dev/null +++ b/src/main/java/cn/hutool/core/date/format/package-info.java @@ -0,0 +1,7 @@ +/** + * 提供线程安全的日期格式的格式化和解析实现 + * + * @author looly + * + */ +package cn.hutool.core.date.format; \ No newline at end of file diff --git a/src/main/java/cn/hutool/core/date/package-info.java b/src/main/java/cn/hutool/core/date/package-info.java new file mode 100644 index 0000000..284d053 --- /dev/null +++ b/src/main/java/cn/hutool/core/date/package-info.java @@ -0,0 +1,7 @@ +/** + * 日期封装,日期的核心为DateTime类,DateUtil提供日期操作的入口 + * + * @author looly + * + */ +package cn.hutool.core.date; \ No newline at end of file diff --git a/src/main/java/cn/hutool/core/exceptions/CheckedUtil.java b/src/main/java/cn/hutool/core/exceptions/CheckedUtil.java new file mode 100644 index 0000000..ae59516 --- /dev/null +++ b/src/main/java/cn/hutool/core/exceptions/CheckedUtil.java @@ -0,0 +1,325 @@ +package cn.hutool.core.exceptions; + +import cn.hutool.core.lang.func.*; + +import java.util.Objects; + +/** + * 方便的执行会抛出受检查类型异常的方法调用或者代码段 + *

+ * 该工具通过函数式的方式将那些需要抛出受检查异常的表达式或者代码段转化成一个 cn.hutool.core.lang.func.Func* 对象 + *

+ *

+ * {@code + *

+ *      //代码中如果遇到一个方法调用声明了受检查异常那么我们的代码就必须这样写
+ *         Map describedObject = null;
+ *         try {
+ *             describe = BeanUtils.describe(new Object());
+ *         } catch (IllegalAccessException e) {
+ *             throw new RuntimeException(e);
+ *         } catch (InvocationTargetException e) {
+ *             throw new RuntimeException(e);
+ *         } catch (NoSuchMethodException e) {
+ *             throw new RuntimeException(e);
+ *         }
+ *         // use describedObject ...
+ *
+ *       //上面的代码增加了异常块使得代码不那么流畅,现在可以这样写:
+ *       Map describedObject = CheckedUtil.uncheck(BeanUtils::describe).call(new Object());
+ *       // use describedObject ...
+ *
+ *       CheckedUtil.uncheck 方法接受任意可以转化成 cn.hutool.core.lang.func.Func* 函数式接口的 Lambda 表达式。返回对应的函数式对象。
+ *       上述代码可以理解为:
+ *        Func0> aFunc = CheckedUtil.uncheck(BeanUtils::describe);
+ *        Map describedObject = aFunc.call(传入参数);
+ *        该aFunc对象代表的就是BeanUtils::describe这个表达式,且在内部转化了检查类型异常,不需要代码里面显示处理。
+ *
+ *
+ * 
+ * } + * + * @author conder + * @since 5.7.19 + */ +public class CheckedUtil { + + /** + * 接收一个可以转化成 cn.hutool.core.lang.func.Func 的Lambda表达式,当执行表达式抛出任何异常的时候,都会转化成运行时异常 + * 如此一来,代码中就不用显示的try-catch转化成运行时异常 + * + * @param expression Lambda表达式 + * @param

运行时传入的参数类型 + * @param 最终返回的数据类型 + * @return {@link FuncRt} + */ + public static FuncRt uncheck(Func expression) { + return uncheck(expression, RuntimeException::new); + } + + /** + * 接收一个可以转化成 cn.hutool.core.lang.func.Func0 的Lambda表达式,当执行表达式抛出任何异常的时候,都会转化成运行时异常 + * 如此一来,代码中就不用显示的try-catch转化成运行时异常 + * + * @param expression 运行时传入的参数类型 + * @param 最终返回的数据类型 + * @return {@link Func0Rt} + */ + public static Func0Rt uncheck(Func0 expression) { + return uncheck(expression, RuntimeException::new); + } + + /** + * 接收一个可以转化成 cn.hutool.core.lang.func.Func1 的Lambda表达式,当执行表达式抛出任何异常的时候,都会转化成运行时异常 + * 如此一来,代码中就不用显示的try-catch转化成运行时异常 + * + * @param expression 运行时传入的参数类型 + * @param

运行时传入的参数类型 + * @param 最终返回的数据类型 + * @return {@link Func1Rt} + */ + public static Func1Rt uncheck(Func1 expression) { + return uncheck(expression, RuntimeException::new); + } + + + /** + * 接收一个可以转化成 cn.hutool.core.lang.func.VoidFunc 的Lambda表达式,当执行表达式抛出任何异常的时候,都会转化成运行时异常 + * 如此一来,代码中就不用显示的try-catch转化成运行时异常 + * + * @param expression 运行时传入的参数类型 + * @param

运行时传入的参数类型 + * @return {@link VoidFuncRt} + */ + public static

VoidFuncRt

uncheck(VoidFunc

expression) { + return uncheck(expression, RuntimeException::new); + } + + /** + * 接收一个可以转化成 cn.hutool.core.lang.func.VoidFunc0 的Lambda表达式,当执行表达式抛出任何异常的时候,都会转化成运行时异常 + * 如此一来,代码中就不用显示的try-catch转化成运行时异常 + * + * @param expression 运行时传入的参数类型 + * @return {@link VoidFunc0Rt} + */ + public static VoidFunc0Rt uncheck(VoidFunc0 expression) { + return uncheck(expression, RuntimeException::new); + } + + /** + * 接收一个可以转化成 cn.hutool.core.lang.func.VoidFunc1 的Lambda表达式,当执行表达式抛出任何异常的时候,都会转化成运行时异常 + * 如此一来,代码中就不用显示的try-catch转化成运行时异常 + * + * @param expression 运行时传入的参数类型 + * @param

运行时传入的参数类型 + * @return {@link VoidFunc1Rt} + */ + public static

VoidFunc1Rt

uncheck(VoidFunc1

expression) { + return uncheck(expression, RuntimeException::new); + } + + + /** + * 接收一个可以转化成 cn.hutool.core.lang.func.Func的Lambda表达式,和一个可以把Exception转化成RuntimeExceptionde的表达式,当执行表达式抛出任何异常的时候,都会转化成运行时异常 + * 如此一来,代码中就不用显示的try-catch转化成运行时异常 + * + * @param expression Lambda表达式 + * @param rteSupplier 转化运行时异常的表达式 + * @param

运行时传入的参数类型 + * @param 最终返回的数据类型 + * @return {@link FuncRt} + */ + public static FuncRt uncheck(Func expression, Supplier1 rteSupplier) { + Objects.requireNonNull(expression, "expression can not be null"); + return t -> { + try { + return expression.call(t); + } catch (Exception e) { + if (rteSupplier == null) { + throw new RuntimeException(e); + } else { + throw rteSupplier.get(e); + } + } + }; + } + + /** + * 接收一个可以转化成 cn.hutool.core.lang.func.Func0的Lambda表达式,和一个可以把Exception转化成RuntimeExceptionde的表达式,当执行表达式抛出任何异常的时候,都会转化成运行时异常 + * 如此一来,代码中就不用显示的try-catch转化成运行时异常 + * + * @param expression Lambda表达式 + * @param rteSupplier 转化运行时异常的表达式 + * @param 最终返回的数据类型 + * @return {@link Func0Rt} + */ + public static Func0Rt uncheck(Func0 expression, Supplier1 rteSupplier) { + Objects.requireNonNull(expression, "expression can not be null"); + return () -> { + try { + return expression.call(); + } catch (Exception e) { + if (rteSupplier == null) { + throw new RuntimeException(e); + } else { + throw rteSupplier.get(e); + } + } + }; + } + + /** + * 接收一个可以转化成 cn.hutool.core.lang.func.Func1的Lambda表达式,和一个可以把Exception转化成RuntimeExceptionde的表达式,当执行表达式抛出任何异常的时候,都会转化成运行时异常 + * 如此一来,代码中就不用显示的try-catch转化成运行时异常 + * + * @param expression Lambda表达式 + * @param rteSupplier 转化运行时异常的表达式 + * @param

运行时传入的参数类型 + * @param 最终返回的数据类型 + * @return {@link Func1Rt} + */ + public static Func1Rt uncheck(Func1 expression, Supplier1 rteSupplier) { + Objects.requireNonNull(expression, "expression can not be null"); + return t -> { + try { + return expression.call(t); + } catch (Exception e) { + if (rteSupplier == null) { + throw new RuntimeException(e); + } else { + throw rteSupplier.get(e); + } + } + }; + } + + /** + * 接收一个可以转化成 cn.hutool.core.lang.func.VoidFunc的Lambda表达式,和一个可以把Exception转化成RuntimeExceptionde的表达式,当执行表达式抛出任何异常的时候,都会转化成运行时异常 + * 如此一来,代码中就不用显示的try-catch转化成运行时异常 + * + * @param expression Lambda表达式 + * @param rteSupplier 转化运行时异常的表达式 + * @param

运行时传入的参数类型 + * @return {@link VoidFuncRt} + */ + public static

VoidFuncRt

uncheck(VoidFunc

expression, Supplier1 rteSupplier) { + Objects.requireNonNull(expression, "expression can not be null"); + return t -> { + try { + expression.call(t); + } catch (Exception e) { + if (rteSupplier == null) { + throw new RuntimeException(e); + } else { + throw rteSupplier.get(e); + } + } + }; + } + + + /** + * 接收一个可以转化成 cn.hutool.core.lang.func.VoidFunc0的Lambda表达式,和一个RuntimeException,当执行表达式抛出任何异常的时候,都会转化成运行时异常 + * 如此一来,代码中就不用显示的try-catch转化成运行时异常 + * + * @param expression Lambda表达式 + * @param rte 期望抛出的运行时异常 + * @return {@link VoidFunc0Rt} + */ + public static VoidFunc0Rt uncheck(VoidFunc0 expression, RuntimeException rte) { + Objects.requireNonNull(expression, "expression can not be null"); + return () -> { + try { + expression.call(); + } catch (Exception e) { + if (rte == null) { + throw new RuntimeException(e); + } else { + rte.initCause(e); + throw rte; + } + } + }; + } + + /** + * 接收一个可以转化成 cn.hutool.core.lang.func.VoidFunc0的Lambda表达式,和一个可以把Exception转化成RuntimeExceptionde的表达式,当执行表达式抛出任何异常的时候,都会转化成运行时异常 + * 如此一来,代码中就不用显示的try-catch转化成运行时异常 + * + * @param expression Lambda表达式 + * @param rteSupplier 转化运行时异常的表达式 + * @return {@link VoidFunc0Rt} + */ + public static VoidFunc0Rt uncheck(VoidFunc0 expression, Supplier1 rteSupplier) { + Objects.requireNonNull(expression, "expression can not be null"); + return () -> { + try { + expression.call(); + } catch (Exception e) { + if (rteSupplier == null) { + throw new RuntimeException(e); + } else { + throw rteSupplier.get(e); + } + } + }; + } + + /** + * 接收一个可以转化成 cn.hutool.core.lang.func.VoidFunc1的Lambda表达式,和一个RuntimeException,当执行表达式抛出任何异常的时候,都会转化成运行时异常 + * 如此一来,代码中就不用显示的try-catch转化成运行时异常 + * + * @param expression Lambda表达式 + * @param rteSupplier 转化运行时异常的表达式 + * @param

运行时传入的参数类型 + * @return {@link VoidFunc1Rt} + */ + public static

VoidFunc1Rt

uncheck(VoidFunc1

expression, Supplier1 rteSupplier) { + Objects.requireNonNull(expression, "expression can not be null"); + return t -> { + try { + expression.call(t); + } catch (Exception e) { + if (rteSupplier == null) { + throw new RuntimeException(e); + } else { + throw rteSupplier.get(e); + } + } + }; + } + + public interface FuncRt extends Func { + @SuppressWarnings("unchecked") + @Override + R call(P... parameters) throws RuntimeException; + } + + public interface Func0Rt extends Func0 { + @Override + R call() throws RuntimeException; + } + + public interface Func1Rt extends Func1 { + @Override + R call(P parameter) throws RuntimeException; + } + + public interface VoidFuncRt

extends VoidFunc

{ + @SuppressWarnings("unchecked") + @Override + void call(P... parameters) throws RuntimeException; + } + + public interface VoidFunc0Rt extends VoidFunc0 { + @Override + void call() throws RuntimeException; + } + + public interface VoidFunc1Rt

extends VoidFunc1

{ + @Override + void call(P parameter) throws RuntimeException; + } + + +} diff --git a/src/main/java/cn/hutool/core/exceptions/DependencyException.java b/src/main/java/cn/hutool/core/exceptions/DependencyException.java new file mode 100644 index 0000000..b2040a2 --- /dev/null +++ b/src/main/java/cn/hutool/core/exceptions/DependencyException.java @@ -0,0 +1,37 @@ +package cn.hutool.core.exceptions; + +import cn.hutool.core.util.StrUtil; + +/** + * 依赖异常 + * + * @author xiaoleilu + * @since 4.0.10 + */ +public class DependencyException extends RuntimeException { + private static final long serialVersionUID = 8247610319171014183L; + + public DependencyException(Throwable e) { + super(ExceptionUtil.getMessage(e), e); + } + + public DependencyException(String message) { + super(message); + } + + public DependencyException(String messageTemplate, Object... params) { + super(StrUtil.format(messageTemplate, params)); + } + + public DependencyException(String message, Throwable throwable) { + super(message, throwable); + } + + public DependencyException(String message, Throwable throwable, boolean enableSuppression, boolean writableStackTrace) { + super(message, throwable, enableSuppression, writableStackTrace); + } + + public DependencyException(Throwable throwable, String messageTemplate, Object... params) { + super(StrUtil.format(messageTemplate, params), throwable); + } +} diff --git a/src/main/java/cn/hutool/core/exceptions/ExceptionUtil.java b/src/main/java/cn/hutool/core/exceptions/ExceptionUtil.java new file mode 100644 index 0000000..d131757 --- /dev/null +++ b/src/main/java/cn/hutool/core/exceptions/ExceptionUtil.java @@ -0,0 +1,428 @@ +package cn.hutool.core.exceptions; + +import cn.hutool.core.io.FastByteArrayOutputStream; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.ReflectUtil; +import cn.hutool.core.util.StrUtil; + +import java.io.PrintStream; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.UndeclaredThrowableException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * 异常工具类 + * + * @author Looly + */ +public class ExceptionUtil { + + /** + * 获得完整消息,包括异常名,消息格式为:{SimpleClassName}: {ThrowableMessage} + * + * @param e 异常 + * @return 完整消息 + */ + public static String getMessage(Throwable e) { + if (null == e) { + return StrUtil.NULL; + } + return StrUtil.format("{}: {}", e.getClass().getSimpleName(), e.getMessage()); + } + + /** + * 获得消息,调用异常类的getMessage方法 + * + * @param e 异常 + * @return 消息 + */ + public static String getSimpleMessage(Throwable e) { + return (null == e) ? StrUtil.NULL : e.getMessage(); + } + + /** + * 使用运行时异常包装编译异常
+ *

+ * 如果传入参数已经是运行时异常,则直接返回,不再额外包装 + * + * @param throwable 异常 + * @return 运行时异常 + */ + public static RuntimeException wrapRuntime(Throwable throwable) { + if (throwable instanceof RuntimeException) { + return (RuntimeException) throwable; + } + return new RuntimeException(throwable); + } + + /** + * 将指定的消息包装为运行时异常 + * + * @param message 异常消息 + * @return 运行时异常 + * @since 5.5.2 + */ + public static RuntimeException wrapRuntime(String message) { + return new RuntimeException(message); + } + + /** + * 包装一个异常 + * + * @param 被包装的异常类型 + * @param throwable 异常 + * @param wrapThrowable 包装后的异常类 + * @return 包装后的异常 + * @since 3.3.0 + */ + @SuppressWarnings("unchecked") + public static T wrap(Throwable throwable, Class wrapThrowable) { + if (wrapThrowable.isInstance(throwable)) { + return (T) throwable; + } + return ReflectUtil.newInstance(wrapThrowable, throwable); + } + + /** + * 包装异常并重新抛出此异常
+ * {@link RuntimeException} 和{@link Error} 直接抛出,其它检查异常包装为{@link UndeclaredThrowableException} 后抛出 + * + * @param throwable 异常 + */ + public static void wrapAndThrow(Throwable throwable) { + if (throwable instanceof RuntimeException) { + throw (RuntimeException) throwable; + } + if (throwable instanceof Error) { + throw (Error) throwable; + } + throw new UndeclaredThrowableException(throwable); + } + + /** + * 将消息包装为运行时异常并抛出 + * + * @param message 异常消息 + * @since 5.5.2 + */ + public static void wrapRuntimeAndThrow(String message) { + throw new RuntimeException(message); + } + + /** + * 剥离反射引发的InvocationTargetException、UndeclaredThrowableException中间异常,返回业务本身的异常 + * + * @param wrapped 包装的异常 + * @return 剥离后的异常 + */ + public static Throwable unwrap(Throwable wrapped) { + Throwable unwrapped = wrapped; + while (true) { + if (unwrapped instanceof InvocationTargetException) { + unwrapped = ((InvocationTargetException) unwrapped).getTargetException(); + } else if (unwrapped instanceof UndeclaredThrowableException) { + unwrapped = ((UndeclaredThrowableException) unwrapped).getUndeclaredThrowable(); + } else { + return unwrapped; + } + } + } + + /** + * 获取当前栈信息 + * + * @return 当前栈信息 + */ + public static StackTraceElement[] getStackElements() { + // return (new Throwable()).getStackTrace(); + return Thread.currentThread().getStackTrace(); + } + + /** + * 获取指定层的堆栈信息 + * + * @param i 层数 + * @return 指定层的堆栈信息 + * @since 4.1.4 + */ + public static StackTraceElement getStackElement(int i) { + return Thread.currentThread().getStackTrace()[i]; + } + + /** + * 获取指定层的堆栈信息 + * + * @param fqcn 指定类名为基础 + * @param i 指定类名的类堆栈相对层数 + * @return 指定层的堆栈信息 + * @since 5.6.6 + */ + public static StackTraceElement getStackElement(String fqcn, int i) { + final StackTraceElement[] stackTraceArray = Thread.currentThread().getStackTrace(); + final int index = ArrayUtil.matchIndex((ele) -> StrUtil.equals(fqcn, ele.getClassName()), stackTraceArray); + if(index > 0){ + return stackTraceArray[index + i]; + } + + return null; + } + + /** + * 获取入口堆栈信息 + * + * @return 入口堆栈信息 + * @since 4.1.4 + */ + public static StackTraceElement getRootStackElement() { + final StackTraceElement[] stackElements = Thread.currentThread().getStackTrace(); + return Thread.currentThread().getStackTrace()[stackElements.length - 1]; + } + + /** + * 堆栈转为单行完整字符串 + * + * @param throwable 异常对象 + * @return 堆栈转为的字符串 + */ + public static String stacktraceToOneLineString(Throwable throwable) { + return stacktraceToOneLineString(throwable, 3000); + } + + /** + * 堆栈转为单行完整字符串 + * + * @param throwable 异常对象 + * @param limit 限制最大长度 + * @return 堆栈转为的字符串 + */ + public static String stacktraceToOneLineString(Throwable throwable, int limit) { + Map replaceCharToStrMap = new HashMap<>(); + replaceCharToStrMap.put(StrUtil.C_CR, StrUtil.SPACE); + replaceCharToStrMap.put(StrUtil.C_LF, StrUtil.SPACE); + replaceCharToStrMap.put(StrUtil.C_TAB, StrUtil.SPACE); + + return stacktraceToString(throwable, limit, replaceCharToStrMap); + } + + /** + * 堆栈转为完整字符串 + * + * @param throwable 异常对象 + * @return 堆栈转为的字符串 + */ + public static String stacktraceToString(Throwable throwable) { + return stacktraceToString(throwable, 3000); + } + + /** + * 堆栈转为完整字符串 + * + * @param throwable 异常对象 + * @param limit 限制最大长度 + * @return 堆栈转为的字符串 + */ + public static String stacktraceToString(Throwable throwable, int limit) { + return stacktraceToString(throwable, limit, null); + } + + /** + * 堆栈转为完整字符串 + * + * @param throwable 异常对象 + * @param limit 限制最大长度,<0表示不限制长度 + * @param replaceCharToStrMap 替换字符为指定字符串 + * @return 堆栈转为的字符串 + */ + public static String stacktraceToString(Throwable throwable, int limit, Map replaceCharToStrMap) { + final FastByteArrayOutputStream baos = new FastByteArrayOutputStream(); + throwable.printStackTrace(new PrintStream(baos)); + + final String exceptionStr = baos.toString(); + final int length = exceptionStr.length(); + if (limit < 0 || limit > length) { + limit = length; + } + + if (MapUtil.isNotEmpty(replaceCharToStrMap)) { + final StringBuilder sb = StrUtil.builder(); + char c; + String value; + for (int i = 0; i < limit; i++) { + c = exceptionStr.charAt(i); + value = replaceCharToStrMap.get(c); + if (null != value) { + sb.append(value); + } else { + sb.append(c); + } + } + return sb.toString(); + } else { + if(limit == length){ + return exceptionStr; + } + return StrUtil.subPre(exceptionStr, limit); + } + } + + /** + * 判断是否由指定异常类引起 + * + * @param throwable 异常 + * @param causeClasses 定义的引起异常的类 + * @return 是否由指定异常类引起 + * @since 4.1.13 + */ + @SuppressWarnings("unchecked") + public static boolean isCausedBy(Throwable throwable, Class... causeClasses) { + return null != getCausedBy(throwable, causeClasses); + } + + /** + * 获取由指定异常类引起的异常 + * + * @param throwable 异常 + * @param causeClasses 定义的引起异常的类 + * @return 是否由指定异常类引起 + * @since 4.1.13 + */ + @SuppressWarnings("unchecked") + public static Throwable getCausedBy(Throwable throwable, Class... causeClasses) { + Throwable cause = throwable; + while (cause != null) { + for (Class causeClass : causeClasses) { + if (causeClass.isInstance(cause)) { + return cause; + } + } + cause = cause.getCause(); + } + return null; + } + + /** + * 判断指定异常是否来自或者包含指定异常 + * + * @param throwable 异常 + * @param exceptionClass 定义的引起异常的类 + * @return true 来自或者包含 + * @since 4.3.2 + */ + public static boolean isFromOrSuppressedThrowable(Throwable throwable, Class exceptionClass) { + return convertFromOrSuppressedThrowable(throwable, exceptionClass, true) != null; + } + + /** + * 判断指定异常是否来自或者包含指定异常 + * + * @param throwable 异常 + * @param exceptionClass 定义的引起异常的类 + * @param checkCause 判断cause + * @return true 来自或者包含 + * @since 4.4.1 + */ + public static boolean isFromOrSuppressedThrowable(Throwable throwable, Class exceptionClass, boolean checkCause) { + return convertFromOrSuppressedThrowable(throwable, exceptionClass, checkCause) != null; + } + + /** + * 转化指定异常为来自或者包含指定异常 + * + * @param 异常类型 + * @param throwable 异常 + * @param exceptionClass 定义的引起异常的类 + * @return 结果为null 不是来自或者包含 + * @since 4.3.2 + */ + public static T convertFromOrSuppressedThrowable(Throwable throwable, Class exceptionClass) { + return convertFromOrSuppressedThrowable(throwable, exceptionClass, true); + } + + /** + * 转化指定异常为来自或者包含指定异常 + * + * @param 异常类型 + * @param throwable 异常 + * @param exceptionClass 定义的引起异常的类 + * @param checkCause 判断cause + * @return 结果为null 不是来自或者包含 + * @since 4.4.1 + */ + @SuppressWarnings("unchecked") + public static T convertFromOrSuppressedThrowable(Throwable throwable, Class exceptionClass, boolean checkCause) { + if (throwable == null || exceptionClass == null) { + return null; + } + if (exceptionClass.isAssignableFrom(throwable.getClass())) { + return (T) throwable; + } + if (checkCause) { + Throwable cause = throwable.getCause(); + if (cause != null && exceptionClass.isAssignableFrom(cause.getClass())) { + return (T) cause; + } + } + Throwable[] throwables = throwable.getSuppressed(); + if (ArrayUtil.isNotEmpty(throwables)) { + for (Throwable throwable1 : throwables) { + if (exceptionClass.isAssignableFrom(throwable1.getClass())) { + return (T) throwable1; + } + } + } + return null; + } + + /** + * 获取异常链上所有异常的集合,如果{@link Throwable} 对象没有cause,返回只有一个节点的List
+ * 如果传入null,返回空集合 + * + *

+ * 此方法来自Apache-Commons-Lang3 + *

+ * + * @param throwable 异常对象,可以为null + * @return 异常链中所有异常集合 + * @since 4.6.2 + */ + public static List getThrowableList(Throwable throwable) { + final List list = new ArrayList<>(); + while (throwable != null && !list.contains(throwable)) { + list.add(throwable); + throwable = throwable.getCause(); + } + return list; + } + + /** + * 获取异常链中最尾端的异常,即异常最早发生的异常对象。
+ * 此方法通过调用{@link Throwable#getCause()} 直到没有cause为止,如果异常本身没有cause,返回异常本身
+ * 传入null返回也为null + * + *

+ * 此方法来自Apache-Commons-Lang3 + *

+ * + * @param throwable 异常对象,可能为null + * @return 最尾端异常,传入null参数返回也为null + */ + public static Throwable getRootCause(final Throwable throwable) { + final List list = getThrowableList(throwable); + return list.size() < 1 ? null : list.get(list.size() - 1); + } + + /** + * 获取异常链中最尾端的异常的消息,消息格式为:{SimpleClassName}: {ThrowableMessage} + * + * @param th 异常 + * @return 消息 + * @since 4.6.2 + */ + public static String getRootCauseMessage(final Throwable th) { + return getMessage(getRootCause(th)); + } +} diff --git a/src/main/java/cn/hutool/core/exceptions/InvocationTargetRuntimeException.java b/src/main/java/cn/hutool/core/exceptions/InvocationTargetRuntimeException.java new file mode 100644 index 0000000..b6444bd --- /dev/null +++ b/src/main/java/cn/hutool/core/exceptions/InvocationTargetRuntimeException.java @@ -0,0 +1,34 @@ +package cn.hutool.core.exceptions; + +/** + * InvocationTargetException的运行时异常 + * + * @author looly + * @since 5.8.1 + */ +public class InvocationTargetRuntimeException extends UtilException { + + public InvocationTargetRuntimeException(Throwable e) { + super(e); + } + + public InvocationTargetRuntimeException(String message) { + super(message); + } + + public InvocationTargetRuntimeException(String messageTemplate, Object... params) { + super(messageTemplate, params); + } + + public InvocationTargetRuntimeException(String message, Throwable throwable) { + super(message, throwable); + } + + public InvocationTargetRuntimeException(String message, Throwable throwable, boolean enableSuppression, boolean writableStackTrace) { + super(message, throwable, enableSuppression, writableStackTrace); + } + + public InvocationTargetRuntimeException(Throwable throwable, String messageTemplate, Object... params) { + super(throwable, messageTemplate, params); + } +} diff --git a/src/main/java/cn/hutool/core/exceptions/NotInitedException.java b/src/main/java/cn/hutool/core/exceptions/NotInitedException.java new file mode 100644 index 0000000..bbe578b --- /dev/null +++ b/src/main/java/cn/hutool/core/exceptions/NotInitedException.java @@ -0,0 +1,36 @@ +package cn.hutool.core.exceptions; + +import cn.hutool.core.util.StrUtil; + +/** + * 未初始化异常 + * + * @author xiaoleilu + */ +public class NotInitedException extends RuntimeException { + private static final long serialVersionUID = 8247610319171014183L; + + public NotInitedException(Throwable e) { + super(e); + } + + public NotInitedException(String message) { + super(message); + } + + public NotInitedException(String messageTemplate, Object... params) { + super(StrUtil.format(messageTemplate, params)); + } + + public NotInitedException(String message, Throwable throwable) { + super(message, throwable); + } + + public NotInitedException(String message, Throwable throwable, boolean enableSuppression, boolean writableStackTrace) { + super(message, throwable, enableSuppression, writableStackTrace); + } + + public NotInitedException(Throwable throwable, String messageTemplate, Object... params) { + super(StrUtil.format(messageTemplate, params), throwable); + } +} diff --git a/src/main/java/cn/hutool/core/exceptions/StatefulException.java b/src/main/java/cn/hutool/core/exceptions/StatefulException.java new file mode 100644 index 0000000..f560dfb --- /dev/null +++ b/src/main/java/cn/hutool/core/exceptions/StatefulException.java @@ -0,0 +1,60 @@ +package cn.hutool.core.exceptions; + +import cn.hutool.core.util.StrUtil; + +/** + * 带有状态码的异常 + * + * @author xiaoleilu + */ +public class StatefulException extends RuntimeException { + private static final long serialVersionUID = 6057602589533840889L; + + // 异常状态码 + private int status; + + public StatefulException() { + } + + public StatefulException(String msg) { + super(msg); + } + + public StatefulException(String messageTemplate, Object... params) { + super(StrUtil.format(messageTemplate, params)); + } + + public StatefulException(Throwable throwable) { + super(throwable); + } + + public StatefulException(String msg, Throwable throwable) { + super(msg, throwable); + } + + public StatefulException(String message, Throwable throwable, boolean enableSuppression, boolean writableStackTrace) { + super(message, throwable, enableSuppression, writableStackTrace); + } + + public StatefulException(int status, String msg) { + super(msg); + this.status = status; + } + + public StatefulException(int status, Throwable throwable) { + super(throwable); + this.status = status; + } + + public StatefulException(int status, String msg, Throwable throwable) { + super(msg, throwable); + this.status = status; + } + + /** + * @return 获得异常状态码 + */ + public int getStatus() { + return status; + } +} diff --git a/src/main/java/cn/hutool/core/exceptions/UtilException.java b/src/main/java/cn/hutool/core/exceptions/UtilException.java new file mode 100644 index 0000000..3024aed --- /dev/null +++ b/src/main/java/cn/hutool/core/exceptions/UtilException.java @@ -0,0 +1,36 @@ +package cn.hutool.core.exceptions; + +import cn.hutool.core.util.StrUtil; + +/** + * 工具类异常 + * + * @author xiaoleilu + */ +public class UtilException extends RuntimeException { + private static final long serialVersionUID = 8247610319171014183L; + + public UtilException(Throwable e) { + super(ExceptionUtil.getMessage(e), e); + } + + public UtilException(String message) { + super(message); + } + + public UtilException(String messageTemplate, Object... params) { + super(StrUtil.format(messageTemplate, params)); + } + + public UtilException(String message, Throwable throwable) { + super(message, throwable); + } + + public UtilException(String message, Throwable throwable, boolean enableSuppression, boolean writableStackTrace) { + super(message, throwable, enableSuppression, writableStackTrace); + } + + public UtilException(Throwable throwable, String messageTemplate, Object... params) { + super(StrUtil.format(messageTemplate, params), throwable); + } +} diff --git a/src/main/java/cn/hutool/core/exceptions/ValidateException.java b/src/main/java/cn/hutool/core/exceptions/ValidateException.java new file mode 100644 index 0000000..98423d6 --- /dev/null +++ b/src/main/java/cn/hutool/core/exceptions/ValidateException.java @@ -0,0 +1,47 @@ +package cn.hutool.core.exceptions; + +import cn.hutool.core.util.StrUtil; + +/** + * 验证异常 + * + * @author xiaoleilu + */ +public class ValidateException extends StatefulException { + private static final long serialVersionUID = 6057602589533840889L; + + public ValidateException() { + } + + public ValidateException(String msg) { + super(msg); + } + + public ValidateException(String messageTemplate, Object... params) { + super(StrUtil.format(messageTemplate, params)); + } + + public ValidateException(Throwable throwable) { + super(throwable); + } + + public ValidateException(String msg, Throwable throwable) { + super(msg, throwable); + } + + public ValidateException(int status, String msg) { + super(status, msg); + } + + public ValidateException(int status, Throwable throwable) { + super(status, throwable); + } + + public ValidateException(String message, Throwable throwable, boolean enableSuppression, boolean writableStackTrace) { + super(message, throwable, enableSuppression, writableStackTrace); + } + + public ValidateException(int status, String msg, Throwable throwable) { + super(status, msg, throwable); + } +} diff --git a/src/main/java/cn/hutool/core/exceptions/package-info.java b/src/main/java/cn/hutool/core/exceptions/package-info.java new file mode 100644 index 0000000..3248a14 --- /dev/null +++ b/src/main/java/cn/hutool/core/exceptions/package-info.java @@ -0,0 +1,7 @@ +/** + * 特殊异常封装,同时提供异常工具ExceptionUtil + * + * @author looly + * + */ +package cn.hutool.core.exceptions; \ No newline at end of file diff --git a/src/main/java/cn/hutool/core/getter/ArrayTypeGetter.java b/src/main/java/cn/hutool/core/getter/ArrayTypeGetter.java new file mode 100644 index 0000000..67b8949 --- /dev/null +++ b/src/main/java/cn/hutool/core/getter/ArrayTypeGetter.java @@ -0,0 +1,102 @@ +package cn.hutool.core.getter; + +import java.math.BigDecimal; +import java.math.BigInteger; + +/** + * 数组类型的Get接口 + * @author Looly + * + */ +public interface ArrayTypeGetter { + /*-------------------------- 数组类型 start -------------------------------*/ + + /** + * 获取Object型属性值数组 + * + * @param key 属性名 + * @return 属性值列表 + */ + String[] getObjs(String key); + + /** + * 获取String型属性值数组 + * + * @param key 属性名 + * @return 属性值列表 + */ + String[] getStrs(String key); + + /** + * 获取Integer型属性值数组 + * + * @param key 属性名 + * @return 属性值列表 + */ + Integer[] getInts(String key); + + /** + * 获取Short型属性值数组 + * + * @param key 属性名 + * @return 属性值列表 + */ + Short[] getShorts(String key); + + /** + * 获取Boolean型属性值数组 + * + * @param key 属性名 + * @return 属性值列表 + */ + Boolean[] getBools(String key); + + /** + * 获取Long型属性值数组 + * + * @param key 属性名 + * @return 属性值列表 + */ + Long[] getLongs(String key); + + /** + * 获取Character型属性值数组 + * + * @param key 属性名 + * @return 属性值列表 + */ + Character[] getChars(String key); + + /** + * 获取Double型属性值数组 + * + * @param key 属性名 + * @return 属性值列表 + */ + Double[] getDoubles(String key); + + /** + * 获取Byte型属性值数组 + * + * @param key 属性名 + * @return 属性值列表 + */ + Byte[] getBytes(String key); + + /** + * 获取BigInteger型属性值数组 + * + * @param key 属性名 + * @return 属性值列表 + */ + BigInteger[] getBigIntegers(String key); + + /** + * 获取BigDecimal型属性值数组 + * + * @param key 属性名 + * @return 属性值列表 + */ + BigDecimal[] getBigDecimals(String key); + /*-------------------------- 数组类型 end -------------------------------*/ +} diff --git a/src/main/java/cn/hutool/core/getter/BasicTypeGetter.java b/src/main/java/cn/hutool/core/getter/BasicTypeGetter.java new file mode 100644 index 0000000..55cfaed --- /dev/null +++ b/src/main/java/cn/hutool/core/getter/BasicTypeGetter.java @@ -0,0 +1,131 @@ +package cn.hutool.core.getter; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.Date; + +/** + * 基本类型的getter接口
+ * 提供一个统一的接口定义返回不同类型的值(基本类型)
+ * + * @author Looly + * @param key类型 + */ +public interface BasicTypeGetter { + /*-------------------------- 基本类型 start -------------------------------*/ + + /** + * 获取Object属性值 + * + * @param key 属性名 + * @return 属性值 + */ + Object getObj(K key); + + /** + * 获取字符串型属性值 + * + * @param key 属性名 + * @return 属性值 + */ + String getStr(K key); + + /** + * 获取int型属性值 + * + * @param key 属性名 + * @return 属性值 + */ + Integer getInt(K key); + + /** + * 获取short型属性值 + * + * @param key 属性名 + * @return 属性值 + */ + Short getShort(K key); + + /** + * 获取boolean型属性值 + * + * @param key 属性名 + * @return 属性值 + */ + Boolean getBool(K key); + + /** + * 获取long型属性值 + * + * @param key 属性名 + * @return 属性值 + */ + Long getLong(K key); + + /** + * 获取char型属性值 + * + * @param key 属性名 + * @return 属性值 + */ + Character getChar(K key); + + /** + * 获取float型属性值
+ * + * @param key 属性名 + * @return 属性值 + */ + Float getFloat(K key); + + /** + * 获取double型属性值 + * + * @param key 属性名 + * @return 属性值 + */ + Double getDouble(K key); + + /** + * 获取byte型属性值 + * + * @param key 属性名 + * @return 属性值 + */ + Byte getByte(K key); + + /** + * 获取BigDecimal型属性值 + * + * @param key 属性名 + * @return 属性值 + */ + BigDecimal getBigDecimal(K key); + + /** + * 获取BigInteger型属性值 + * + * @param key 属性名 + * @return 属性值 + */ + BigInteger getBigInteger(K key); + + /** + * 获得Enum类型的值 + * + * @param 枚举类型 + * @param clazz Enum的Class + * @param key KEY + * @return Enum类型的值,无则返回Null + */ + > E getEnum(Class clazz, K key); + + /** + * 获取Date类型值 + * + * @param key 属性名 + * @return Date类型属性值 + */ + Date getDate(K key); + /*-------------------------- 基本类型 end -------------------------------*/ +} diff --git a/src/main/java/cn/hutool/core/getter/GroupedTypeGetter.java b/src/main/java/cn/hutool/core/getter/GroupedTypeGetter.java new file mode 100644 index 0000000..c82f287 --- /dev/null +++ b/src/main/java/cn/hutool/core/getter/GroupedTypeGetter.java @@ -0,0 +1,103 @@ +package cn.hutool.core.getter; + +import java.math.BigDecimal; +import java.math.BigInteger; + +/** + * 基于分组的Get接口 + * @author Looly + * + */ +public interface GroupedTypeGetter { + /*-------------------------- 基本类型 start -------------------------------*/ + /** + * 获取字符串型属性值
+ * + * @param key 属性名 + * @param group 分组 + * @return 属性值 + */ + String getStrByGroup(String key, String group); + + /** + * 获取int型属性值
+ * + * @param key 属性名 + * @param group 分组 + * @return 属性值 + */ + Integer getIntByGroup(String key, String group); + + /** + * 获取short型属性值
+ * + * @param key 属性名 + * @param group 分组 + * @return 属性值 + */ + Short getShortByGroup(String key, String group); + + /** + * 获取boolean型属性值
+ * + * @param key 属性名 + * @param group 分组 + * @return 属性值 + */ + Boolean getBoolByGroup(String key, String group); + + /** + * 获取Long型属性值
+ * + * @param key 属性名 + * @param group 分组 + * @return 属性值 + */ + Long getLongByGroup(String key, String group); + + /** + * 获取char型属性值
+ * + * @param key 属性名 + * @param group 分组 + * @return 属性值 + */ + Character getCharByGroup(String key, String group); + + /** + * 获取double型属性值
+ * + * @param key 属性名 + * @param group 分组 + * @return 属性值 + */ + Double getDoubleByGroup(String key, String group); + + /** + * 获取byte型属性值
+ * + * @param key 属性名 + * @param group 分组 + * @return 属性值 + */ + Byte getByteByGroup(String key, String group); + + /** + * 获取BigDecimal型属性值
+ * + * @param key 属性名 + * @param group 分组 + * @return 属性值 + */ + BigDecimal getBigDecimalByGroup(String key, String group); + + /** + * 获取BigInteger型属性值
+ * + * @param key 属性名 + * @param group 分组 + * @return 属性值 + */ + BigInteger getBigIntegerByGroup(String key, String group); + /*-------------------------- 基本类型 end -------------------------------*/ +} diff --git a/src/main/java/cn/hutool/core/getter/ListTypeGetter.java b/src/main/java/cn/hutool/core/getter/ListTypeGetter.java new file mode 100644 index 0000000..8ab1283 --- /dev/null +++ b/src/main/java/cn/hutool/core/getter/ListTypeGetter.java @@ -0,0 +1,102 @@ +package cn.hutool.core.getter; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.List; + +/** + * 列表类型的Get接口 + * @author Looly + * + */ +public interface ListTypeGetter { + /*-------------------------- List类型 start -------------------------------*/ + /** + * 获取Object型属性值列表 + * + * @param key 属性名 + * @return 属性值列表 + */ + List getObjList(String key); + + /** + * 获取String型属性值列表 + * + * @param key 属性名 + * @return 属性值列表 + */ + List getStrList(String key); + + /** + * 获取Integer型属性值列表 + * + * @param key 属性名 + * @return 属性值列表 + */ + List getIntList(String key); + + /** + * 获取Short型属性值列表 + * + * @param key 属性名 + * @return 属性值列表 + */ + List getShortList(String key); + + /** + * 获取Boolean型属性值列表 + * + * @param key 属性名 + * @return 属性值列表 + */ + List getBoolList(String key); + + /** + * 获取Long型属性值列表 + * + * @param key 属性名 + * @return 属性值列表 + */ + List getLongList(String key); + + /** + * 获取Character型属性值列表 + * + * @param key 属性名 + * @return 属性值列表 + */ + List getCharList(String key); + + /** + * 获取Double型属性值列表 + * + * @param key 属性名 + * @return 属性值列表 + */ + List getDoubleList(String key); + + /** + * 获取Byte型属性值列表 + * + * @param key 属性名 + * @return 属性值列表 + */ + List getByteList(String key); + + /** + * 获取BigDecimal型属性值列表 + * + * @param key 属性名 + * @return 属性值列表 + */ + List getBigDecimalList(String key); + + /** + * 获取BigInteger型属性值列表 + * + * @param key 属性名 + * @return 属性值列表 + */ + List getBigIntegerList(String key); + /*-------------------------- List类型 end -------------------------------*/ +} diff --git a/src/main/java/cn/hutool/core/getter/OptArrayTypeGetter.java b/src/main/java/cn/hutool/core/getter/OptArrayTypeGetter.java new file mode 100644 index 0000000..99e688d --- /dev/null +++ b/src/main/java/cn/hutool/core/getter/OptArrayTypeGetter.java @@ -0,0 +1,117 @@ +package cn.hutool.core.getter; + +import java.math.BigDecimal; +import java.math.BigInteger; + +/** + * 可选默认值的数组类型的Get接口 + * 提供一个统一的接口定义返回不同类型的值(基本类型)
+ * 如果值不存在或获取错误,返回默认值 + * + * @author Looly + * @since 4.0.2 + * + */ +public interface OptArrayTypeGetter { + /*-------------------------- 数组类型 start -------------------------------*/ + + /** + * 获取Object型属性值数组 + * + * @param key 属性名 + * @param defaultValue 默认数组值 + * @return 属性值列表 + */ + Object[] getObjs(String key, Object[] defaultValue); + + /** + * 获取String型属性值数组 + * + * @param key 属性名 + * @param defaultValue 默认数组值 + * @return 属性值列表 + */ + String[] getStrs(String key, String[] defaultValue); + + /** + * 获取Integer型属性值数组 + * + * @param key 属性名 + * @param defaultValue 默认数组值 + * @return 属性值列表 + */ + Integer[] getInts(String key, Integer[] defaultValue); + + /** + * 获取Short型属性值数组 + * + * @param key 属性名 + * @param defaultValue 默认数组值 + * @return 属性值列表 + */ + Short[] getShorts(String key, Short[] defaultValue); + + /** + * 获取Boolean型属性值数组 + * + * @param key 属性名 + * @param defaultValue 默认数组值 + * @return 属性值列表 + */ + Boolean[] getBools(String key, Boolean[] defaultValue); + + /** + * 获取Long型属性值数组 + * + * @param key 属性名 + * @param defaultValue 默认数组值 + * @return 属性值列表 + */ + Long[] getLongs(String key, Long[] defaultValue); + + /** + * 获取Character型属性值数组 + * + * @param key 属性名 + * @param defaultValue 默认数组值 + * @return 属性值列表 + */ + Character[] getChars(String key, Character[] defaultValue); + + /** + * 获取Double型属性值数组 + * + * @param key 属性名 + * @param defaultValue 默认数组值 + * @return 属性值列表 + */ + Double[] getDoubles(String key, Double[] defaultValue); + + /** + * 获取Byte型属性值数组 + * + * @param key 属性名 + * @param defaultValue 默认数组值 + * @return 属性值列表 + */ + Byte[] getBytes(String key, Byte[] defaultValue); + + /** + * 获取BigInteger型属性值数组 + * + * @param key 属性名 + * @param defaultValue 默认数组值 + * @return 属性值列表 + */ + BigInteger[] getBigIntegers(String key, BigInteger[] defaultValue); + + /** + * 获取BigDecimal型属性值数组 + * + * @param key 属性名 + * @param defaultValue 默认数组值 + * @return 属性值列表 + */ + BigDecimal[] getBigDecimals(String key, BigDecimal[] defaultValue); + /*-------------------------- 数组类型 end -------------------------------*/ +} diff --git a/src/main/java/cn/hutool/core/getter/OptBasicTypeGetter.java b/src/main/java/cn/hutool/core/getter/OptBasicTypeGetter.java new file mode 100644 index 0000000..073fd66 --- /dev/null +++ b/src/main/java/cn/hutool/core/getter/OptBasicTypeGetter.java @@ -0,0 +1,153 @@ +package cn.hutool.core.getter; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.Date; + +/** + * 可选默认值的基本类型的getter接口
+ * 提供一个统一的接口定义返回不同类型的值(基本类型)
+ * 如果值不存在或获取错误,返回默认值 + * @author Looly + */ +public interface OptBasicTypeGetter { + /*-------------------------- 基本类型 start -------------------------------*/ + + /** + * 获取Object属性值 + * @param key 属性名 + * @param defaultValue 默认值 + * @return 属性值,无对应值返回defaultValue + */ + Object getObj(K key, Object defaultValue); + + /** + * 获取字符串型属性值
+ * 若获得的值为不可见字符,使用默认值 + * + * @param key 属性名 + * @param defaultValue 默认值 + * @return 属性值,无对应值返回defaultValue + */ + String getStr(K key, String defaultValue); + + /** + * 获取int型属性值
+ * 若获得的值为不可见字符,使用默认值 + * + * @param key 属性名 + * @param defaultValue 默认值 + * @return 属性值,无对应值返回defaultValue + */ + Integer getInt(K key, Integer defaultValue); + + /** + * 获取short型属性值
+ * 若获得的值为不可见字符,使用默认值 + * + * @param key 属性名 + * @param defaultValue 默认值 + * @return 属性值,无对应值返回defaultValue + */ + Short getShort(K key, Short defaultValue); + + /** + * 获取boolean型属性值
+ * 若获得的值为不可见字符,使用默认值 + * + * @param key 属性名 + * @param defaultValue 默认值 + * @return 属性值,无对应值返回defaultValue + */ + Boolean getBool(K key, Boolean defaultValue); + + /** + * 获取Long型属性值
+ * 若获得的值为不可见字符,使用默认值 + * + * @param key 属性名 + * @param defaultValue 默认值 + * @return 属性值,无对应值返回defaultValue + */ + Long getLong(K key, Long defaultValue); + + /** + * 获取char型属性值
+ * 若获得的值为不可见字符,使用默认值 + * + * @param key 属性名 + * @param defaultValue 默认值 + * @return 属性值,无对应值返回defaultValue + */ + Character getChar(K key, Character defaultValue); + + /** + * 获取float型属性值
+ * 若获得的值为不可见字符,使用默认值 + * + * @param key 属性名 + * @param defaultValue 默认值 + * @return 属性值,无对应值返回defaultValue + */ + Float getFloat(K key, Float defaultValue); + + /** + * 获取double型属性值
+ * 若获得的值为不可见字符,使用默认值 + * + * @param key 属性名 + * @param defaultValue 默认值 + * @return 属性值,无对应值返回defaultValue + */ + Double getDouble(K key, Double defaultValue); + + /** + * 获取byte型属性值
+ * 若获得的值为不可见字符,使用默认值 + * + * @param key 属性名 + * @param defaultValue 默认值 + * @return 属性值,无对应值返回defaultValue + */ + Byte getByte(K key, Byte defaultValue); + + /** + * 获取BigDecimal型属性值
+ * 若获得的值为不可见字符,使用默认值 + * + * @param key 属性名 + * @param defaultValue 默认值 + * @return 属性值,无对应值返回defaultValue + */ + BigDecimal getBigDecimal(K key, BigDecimal defaultValue); + + /** + * 获取BigInteger型属性值
+ * 若获得的值为不可见字符,使用默认值 + * + * @param key 属性名 + * @param defaultValue 默认值 + * @return 属性值,无对应值返回defaultValue + */ + BigInteger getBigInteger(K key, BigInteger defaultValue); + + /** + * 获得Enum类型的值 + * + * @param 枚举类型 + * @param clazz Enum的Class + * @param key KEY + * @param defaultValue 默认值 + * @return Enum类型的值,无则返回Null + */ + > E getEnum(Class clazz, K key, E defaultValue); + + /** + * 获取Date类型值 + * @param key 属性名 + * @param defaultValue 默认值 + * @return Date类型属性值 + */ + Date getDate(K key, Date defaultValue); + /*-------------------------- 基本类型 end -------------------------------*/ +} diff --git a/src/main/java/cn/hutool/core/getter/OptNullBasicTypeFromObjectGetter.java b/src/main/java/cn/hutool/core/getter/OptNullBasicTypeFromObjectGetter.java new file mode 100644 index 0000000..0b1bf12 --- /dev/null +++ b/src/main/java/cn/hutool/core/getter/OptNullBasicTypeFromObjectGetter.java @@ -0,0 +1,133 @@ +package cn.hutool.core.getter; + +import cn.hutool.core.convert.Convert; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.Date; + +/** + * 基本类型的getter接口抽象实现,所有类型的值获取都是通过将getObj获得的值转换而来
+ * 用户只需实现getObj方法即可,其他类型将会从Object结果中转换 + * 在不提供默认值的情况下, 如果值不存在或获取错误,返回null
+ * + * @author Looly + */ +public interface OptNullBasicTypeFromObjectGetter extends OptNullBasicTypeGetter { + @Override + default String getStr(K key, String defaultValue) { + final Object obj = getObj(key); + if (null == obj) { + return defaultValue; + } + return Convert.toStr(obj, defaultValue); + } + + @Override + default Integer getInt(K key, Integer defaultValue) { + final Object obj = getObj(key); + if (null == obj) { + return defaultValue; + } + return Convert.toInt(obj, defaultValue); + } + + @Override + default Short getShort(K key, Short defaultValue) { + final Object obj = getObj(key); + if (null == obj) { + return defaultValue; + } + return Convert.toShort(obj, defaultValue); + } + + @Override + default Boolean getBool(K key, Boolean defaultValue) { + final Object obj = getObj(key); + if (null == obj) { + return defaultValue; + } + return Convert.toBool(obj, defaultValue); + } + + @Override + default Long getLong(K key, Long defaultValue) { + final Object obj = getObj(key); + if (null == obj) { + return defaultValue; + } + return Convert.toLong(obj, defaultValue); + } + + @Override + default Character getChar(K key, Character defaultValue) { + final Object obj = getObj(key); + if (null == obj) { + return defaultValue; + } + return Convert.toChar(obj, defaultValue); + } + + @Override + default Float getFloat(K key, Float defaultValue) { + final Object obj = getObj(key); + if (null == obj) { + return defaultValue; + } + return Convert.toFloat(obj, defaultValue); + } + + @Override + default Double getDouble(K key, Double defaultValue) { + final Object obj = getObj(key); + if (null == obj) { + return defaultValue; + } + return Convert.toDouble(obj, defaultValue); + } + + @Override + default Byte getByte(K key, Byte defaultValue) { + final Object obj = getObj(key); + if (null == obj) { + return defaultValue; + } + return Convert.toByte(obj, defaultValue); + } + + @Override + default BigDecimal getBigDecimal(K key, BigDecimal defaultValue) { + final Object obj = getObj(key); + if (null == obj) { + return defaultValue; + } + return Convert.toBigDecimal(obj, defaultValue); + } + + @Override + default BigInteger getBigInteger(K key, BigInteger defaultValue) { + final Object obj = getObj(key); + if (null == obj) { + return defaultValue; + } + return Convert.toBigInteger(obj, defaultValue); + } + + @Override + default > E getEnum(Class clazz, K key, E defaultValue) { + final Object obj = getObj(key); + if (null == obj) { + return defaultValue; + } + return Convert.toEnum(clazz, obj, defaultValue); + } + + @Override + default Date getDate(K key, Date defaultValue) { + final Object obj = getObj(key); + if (null == obj) { + return defaultValue; + } + return Convert.toDate(obj, defaultValue); + } +} diff --git a/src/main/java/cn/hutool/core/getter/OptNullBasicTypeFromStringGetter.java b/src/main/java/cn/hutool/core/getter/OptNullBasicTypeFromStringGetter.java new file mode 100644 index 0000000..b09505d --- /dev/null +++ b/src/main/java/cn/hutool/core/getter/OptNullBasicTypeFromStringGetter.java @@ -0,0 +1,80 @@ +package cn.hutool.core.getter; + +import cn.hutool.core.convert.Convert; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.Date; + +/** + * 基本类型的getter接口抽象实现,所有类型的值获取都是通过将String转换而来
+ * 用户只需实现getStr方法即可,其他类型将会从String结果中转换 在不提供默认值的情况下, 如果值不存在或获取错误,返回null
+ * + * @author Looly + */ +public interface OptNullBasicTypeFromStringGetter extends OptNullBasicTypeGetter { + @Override + default Object getObj(K key, Object defaultValue) { + return getStr(key, null == defaultValue ? null : defaultValue.toString()); + } + + @Override + default Integer getInt(K key, Integer defaultValue) { + return Convert.toInt(getStr(key), defaultValue); + } + + @Override + default Short getShort(K key, Short defaultValue) { + return Convert.toShort(getStr(key), defaultValue); + } + + @Override + default Boolean getBool(K key, Boolean defaultValue) { + return Convert.toBool(getStr(key), defaultValue); + } + + @Override + default Long getLong(K key, Long defaultValue) { + return Convert.toLong(getStr(key), defaultValue); + } + + @Override + default Character getChar(K key, Character defaultValue) { + return Convert.toChar(getStr(key), defaultValue); + } + + @Override + default Float getFloat(K key, Float defaultValue) { + return Convert.toFloat(getStr(key), defaultValue); + } + + @Override + default Double getDouble(K key, Double defaultValue) { + return Convert.toDouble(getStr(key), defaultValue); + } + + @Override + default Byte getByte(K key, Byte defaultValue) { + return Convert.toByte(getStr(key), defaultValue); + } + + @Override + default BigDecimal getBigDecimal(K key, BigDecimal defaultValue) { + return Convert.toBigDecimal(getStr(key), defaultValue); + } + + @Override + default BigInteger getBigInteger(K key, BigInteger defaultValue) { + return Convert.toBigInteger(getStr(key), defaultValue); + } + + @Override + default > E getEnum(Class clazz, K key, E defaultValue) { + return Convert.toEnum(clazz, getStr(key), defaultValue); + } + + @Override + default Date getDate(K key, Date defaultValue) { + return Convert.toDate(getStr(key), defaultValue); + } +} diff --git a/src/main/java/cn/hutool/core/getter/OptNullBasicTypeGetter.java b/src/main/java/cn/hutool/core/getter/OptNullBasicTypeGetter.java new file mode 100644 index 0000000..3c55b7d --- /dev/null +++ b/src/main/java/cn/hutool/core/getter/OptNullBasicTypeGetter.java @@ -0,0 +1,176 @@ +package cn.hutool.core.getter; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.Date; + +/** + * 基本类型的getter接口抽象实现
+ * 提供一个统一的接口定义返回不同类型的值(基本类型)
+ * 在不提供默认值的情况下, 如果值不存在或获取错误,返回null
+ * 用户只需实现{@link OptBasicTypeGetter}接口即可 + * @author Looly + */ +public interface OptNullBasicTypeGetter extends BasicTypeGetter, OptBasicTypeGetter{ + @Override + default Object getObj(K key) { + return getObj(key, null); + } + + /** + * 获取字符串型属性值
+ * 无值或获取错误返回null + * + * @param key 属性名 + * @return 属性值 + */ + @Override + default String getStr(K key){ + return this.getStr(key, null); + } + + /** + * 获取int型属性值
+ * 无值或获取错误返回null + * + * @param key 属性名 + * @return 属性值 + */ + @Override + default Integer getInt(K key) { + return this.getInt(key, null); + } + + /** + * 获取short型属性值
+ * 无值或获取错误返回null + * + * @param key 属性名 + * @return 属性值 + */ + @Override + default Short getShort(K key){ + return this.getShort(key, null); + } + + /** + * 获取boolean型属性值
+ * 无值或获取错误返回null + * + * @param key 属性名 + * @return 属性值 + */ + @Override + default Boolean getBool(K key){ + return this.getBool(key, null); + } + + /** + * 获取long型属性值
+ * 无值或获取错误返回null + * + * @param key 属性名 + * @return 属性值 + */ + @Override + default Long getLong(K key){ + return this.getLong(key, null); + } + + /** + * 获取char型属性值
+ * 无值或获取错误返回null + * + * @param key 属性名 + * @return 属性值 + */ + @Override + default Character getChar(K key){ + return this.getChar(key, null); + } + + /** + * 获取float型属性值
+ * 无值或获取错误返回null + * + * @param key 属性名 + * @return 属性值 + */ + @Override + default Float getFloat(K key){ + return this.getFloat(key, null); + } + + /** + * 获取double型属性值
+ * 无值或获取错误返回null + * + * @param key 属性名 + * @return 属性值 + */ + @Override + default Double getDouble(K key){ + return this.getDouble(key, null); + } + + /** + * 获取byte型属性值
+ * 无值或获取错误返回null + * + * @param key 属性名 + * @return 属性值 + */ + @Override + default Byte getByte(K key){ + return this.getByte(key, null); + } + + /** + * 获取BigDecimal型属性值
+ * 无值或获取错误返回null + * + * @param key 属性名 + * @return 属性值 + */ + @Override + default BigDecimal getBigDecimal(K key){ + return this.getBigDecimal(key, null); + } + + /** + * 获取BigInteger型属性值
+ * 无值或获取错误返回null + * + * @param key 属性名 + * @return 属性值 + */ + @Override + default BigInteger getBigInteger(K key){ + return this.getBigInteger(key, null); + } + + /** + * 获取Enum型属性值
+ * 无值或获取错误返回null + * + * @param clazz Enum 的 Class + * @param key 属性名 + * @return 属性值 + */ + @Override + default > E getEnum(Class clazz, K key) { + return this.getEnum(clazz, key, null); + } + + /** + * 获取Date型属性值
+ * 无值或获取错误返回null + * + * @param key 属性名 + * @return 属性值 + */ + @Override + default Date getDate(K key) { + return this.getDate(key, null); + } +} diff --git a/src/main/java/cn/hutool/core/getter/package-info.java b/src/main/java/cn/hutool/core/getter/package-info.java new file mode 100644 index 0000000..ae44c1b --- /dev/null +++ b/src/main/java/cn/hutool/core/getter/package-info.java @@ -0,0 +1,7 @@ +/** + * getXXX方法的接口和抽象实现 + * + * @author looly + * + */ +package cn.hutool.core.getter; \ No newline at end of file diff --git a/src/main/java/cn/hutool/core/io/AppendableWriter.java b/src/main/java/cn/hutool/core/io/AppendableWriter.java new file mode 100644 index 0000000..22e9d4d --- /dev/null +++ b/src/main/java/cn/hutool/core/io/AppendableWriter.java @@ -0,0 +1,106 @@ +package cn.hutool.core.io; + +import java.io.Closeable; +import java.io.Flushable; +import java.io.IOException; +import java.io.Writer; +import java.nio.CharBuffer; + +/** + * 同时继承{@link Writer}和实现{@link Appendable}的聚合类,用于适配两种接口操作 + * 实现来自:jodd + * + * @author looly,jodd + * @since 5.7.0 + */ +public class AppendableWriter extends Writer implements Appendable { + + private final Appendable appendable; + private final boolean flushable; + private boolean closed; + + public AppendableWriter(final Appendable appendable) { + this.appendable = appendable; + this.flushable = appendable instanceof Flushable; + this.closed = false; + } + + @Override + public void write(final char[] cbuf, final int off, final int len) throws IOException { + checkNotClosed(); + appendable.append(CharBuffer.wrap(cbuf), off, off + len); + } + + @Override + public void write(final int c) throws IOException { + checkNotClosed(); + appendable.append((char) c); + } + + @Override + public Writer append(final char c) throws IOException { + checkNotClosed(); + appendable.append(c); + return this; + } + + @Override + public Writer append(final CharSequence csq, final int start, final int end) throws IOException { + checkNotClosed(); + appendable.append(csq, start, end); + return this; + } + + @Override + public Writer append(final CharSequence csq) throws IOException { + checkNotClosed(); + appendable.append(csq); + return this; + } + + @Override + public void write(final String str, final int off, final int len) throws IOException { + checkNotClosed(); + appendable.append(str, off, off + len); + } + + @Override + public void write(final String str) throws IOException { + appendable.append(str); + } + + @Override + public void write(final char[] cbuf) throws IOException { + appendable.append(CharBuffer.wrap(cbuf)); + } + + @Override + public void flush() throws IOException { + checkNotClosed(); + if (flushable) { + ((Flushable) appendable).flush(); + } + } + + /** + * 检查Writer是否已经被关闭 + * + * @throws IOException IO异常 + */ + private void checkNotClosed() throws IOException { + if (closed) { + throw new IOException("Writer is closed!" + this); + } + } + + @Override + public void close() throws IOException { + if (!closed) { + flush(); + if (appendable instanceof Closeable) { + ((Closeable) appendable).close(); + } + closed = true; + } + } +} diff --git a/src/main/java/cn/hutool/core/io/BOMInputStream.java b/src/main/java/cn/hutool/core/io/BOMInputStream.java new file mode 100644 index 0000000..c927dfb --- /dev/null +++ b/src/main/java/cn/hutool/core/io/BOMInputStream.java @@ -0,0 +1,140 @@ +package cn.hutool.core.io; + +import cn.hutool.core.util.CharsetUtil; + +import java.io.IOException; +import java.io.InputStream; +import java.io.PushbackInputStream; + +/** + * 读取带BOM头的流内容,{@code getCharset()}方法调用后会得到BOM头的编码,且会去除BOM头
+ * BOM定义:http://www.unicode.org/unicode/faq/utf_bom.html
+ *
    + *
  • 00 00 FE FF = UTF-32, big-endian
  • + *
  • FF FE 00 00 = UTF-32, little-endian
  • + *
  • EF BB BF = UTF-8
  • + *
  • FE FF = UTF-16, big-endian
  • + *
  • FF FE = UTF-16, little-endian
  • + *
+ * 使用:
+ * + * String enc = "UTF-8"; // or NULL to use systemdefault
+ * FileInputStream fis = new FileInputStream(file);
+ * BOMInputStream uin = new BOMInputStream(fis, enc);
+ * enc = uin.getCharset(); // check and skip possible BOM bytes + *
+ *

+ * 参考: http://akini.mbnet.fi/java/unicodereader/UnicodeInputStream.java.txt + * + * @author looly + */ +public class BOMInputStream extends InputStream { + + private final PushbackInputStream in; + private boolean isInited = false; + private final String defaultCharset; + private String charset; + + private static final int BOM_SIZE = 4; + + // ----------------------------------------------------------------- Constructor start + + /** + * 构造 + * @param in 流 + */ + public BOMInputStream(InputStream in) { + this(in, CharsetUtil.UTF_8); + } + + /** + * 构造 + * + * @param in 流 + * @param defaultCharset 默认编码 + */ + public BOMInputStream(InputStream in, String defaultCharset) { + this.in = new PushbackInputStream(in, BOM_SIZE); + this.defaultCharset = defaultCharset; + } + // ----------------------------------------------------------------- Constructor end + + /** + * 获取默认编码 + * + * @return 默认编码 + */ + public String getDefaultCharset() { + return defaultCharset; + } + + /** + * 获取BOM头中的编码 + * + * @return 编码 + */ + public String getCharset() { + if (!isInited) { + try { + init(); + } catch (IOException ex) { + throw new IORuntimeException(ex); + } + } + return charset; + } + + @Override + public void close() throws IOException { + isInited = true; + in.close(); + } + + @Override + public int read() throws IOException { + isInited = true; + return in.read(); + } + + /** + * Read-ahead four bytes and check for BOM marks.
+ * Extra bytes are unread back to the stream, only BOM bytes are skipped. + * @throws IOException 读取引起的异常 + */ + protected void init() throws IOException { + if (isInited) { + return; + } + + byte[] bom = new byte[BOM_SIZE]; + int n, unread; + n = in.read(bom, 0, bom.length); + + if ((bom[0] == (byte) 0x00) && (bom[1] == (byte) 0x00) && (bom[2] == (byte) 0xFE) && (bom[3] == (byte) 0xFF)) { + charset = "UTF-32BE"; + unread = n - 4; + } else if ((bom[0] == (byte) 0xFF) && (bom[1] == (byte) 0xFE) && (bom[2] == (byte) 0x00) && (bom[3] == (byte) 0x00)) { + charset = "UTF-32LE"; + unread = n - 4; + } else if ((bom[0] == (byte) 0xEF) && (bom[1] == (byte) 0xBB) && (bom[2] == (byte) 0xBF)) { + charset = "UTF-8"; + unread = n - 3; + } else if ((bom[0] == (byte) 0xFE) && (bom[1] == (byte) 0xFF)) { + charset = "UTF-16BE"; + unread = n - 2; + } else if ((bom[0] == (byte) 0xFF) && (bom[1] == (byte) 0xFE)) { + charset = "UTF-16LE"; + unread = n - 2; + } else { + // Unicode BOM mark not found, unread all bytes + charset = defaultCharset; + unread = n; + } + + if (unread > 0) { + in.unread(bom, (n - unread), unread); + } + + isInited = true; + } +} diff --git a/src/main/java/cn/hutool/core/io/BomReader.java b/src/main/java/cn/hutool/core/io/BomReader.java new file mode 100644 index 0000000..ff28a5e --- /dev/null +++ b/src/main/java/cn/hutool/core/io/BomReader.java @@ -0,0 +1,58 @@ +package cn.hutool.core.io; + +import cn.hutool.core.lang.Assert; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.io.UnsupportedEncodingException; + +/** + * 读取带BOM头的流内容的Reader,如果非bom的流或无法识别的编码,则默认UTF-8
+ * BOM定义:http://www.unicode.org/unicode/faq/utf_bom.html + * + *
    + *
  • 00 00 FE FF = UTF-32, big-endian
  • + *
  • FF FE 00 00 = UTF-32, little-endian
  • + *
  • EF BB BF = UTF-8
  • + *
  • FE FF = UTF-16, big-endian
  • + *
  • FF FE = UTF-16, little-endian
  • + *
+ * 使用:
+ * + * FileInputStream fis = new FileInputStream(file);
+ * BomReader uin = new BomReader(fis);
+ *
+ * + * @author looly + * @since 5.7.14 + */ +public class BomReader extends Reader { + + private InputStreamReader reader; + + /** + * 构造 + * + * @param in 流 + */ + public BomReader(InputStream in) { + Assert.notNull(in, "InputStream must be not null!"); + final BOMInputStream bin = (in instanceof BOMInputStream) ? (BOMInputStream) in : new BOMInputStream(in); + try { + this.reader = new InputStreamReader(bin, bin.getCharset()); + } catch (UnsupportedEncodingException ignore) { + } + } + + @Override + public int read(char[] cbuf, int off, int len) throws IOException { + return reader.read(cbuf, off, len); + } + + @Override + public void close() throws IOException { + reader.close(); + } +} diff --git a/src/main/java/cn/hutool/core/io/BufferUtil.java b/src/main/java/cn/hutool/core/io/BufferUtil.java new file mode 100644 index 0000000..f4e8157 --- /dev/null +++ b/src/main/java/cn/hutool/core/io/BufferUtil.java @@ -0,0 +1,262 @@ +package cn.hutool.core.io; + +import cn.hutool.core.util.CharsetUtil; +import cn.hutool.core.util.StrUtil; + +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.nio.charset.Charset; + +/** + * {@link ByteBuffer} 工具类
+ * 此工具来自于 t-io 项目以及其它项目的相关部分收集
+ * ByteBuffer的相关介绍见:https://www.cnblogs.com/ruber/p/6857159.html + * + * @author tanyaowu, looly + * @since 4.0.0 + */ +public class BufferUtil { + + /** + * 拷贝到一个新的ByteBuffer + * + * @param src 源ByteBuffer + * @param start 起始位置(包括) + * @param end 结束位置(不包括) + * @return 新的ByteBuffer + */ + public static ByteBuffer copy(ByteBuffer src, int start, int end) { + return copy(src, ByteBuffer.allocate(end - start)); + } + + /** + * 拷贝ByteBuffer + * + * @param src 源ByteBuffer + * @param dest 目标ByteBuffer + * @return 目标ByteBuffer + */ + public static ByteBuffer copy(ByteBuffer src, ByteBuffer dest) { + return copy(src, dest, Math.min(src.limit(), dest.remaining())); + } + + /** + * 拷贝ByteBuffer + * + * @param src 源ByteBuffer + * @param dest 目标ByteBuffer + * @param length 长度 + * @return 目标ByteBuffer + */ + public static ByteBuffer copy(ByteBuffer src, ByteBuffer dest, int length) { + return copy(src, src.position(), dest, dest.position(), length); + } + + /** + * 拷贝ByteBuffer + * + * @param src 源ByteBuffer + * @param srcStart 源开始的位置 + * @param dest 目标ByteBuffer + * @param destStart 目标开始的位置 + * @param length 长度 + * @return 目标ByteBuffer + */ + public static ByteBuffer copy(ByteBuffer src, int srcStart, ByteBuffer dest, int destStart, int length) { + System.arraycopy(src.array(), srcStart, dest.array(), destStart, length); + return dest; + } + + /** + * 读取剩余部分并转为UTF-8编码字符串 + * + * @param buffer ByteBuffer + * @return 字符串 + * @since 4.5.0 + */ + public static String readUtf8Str(ByteBuffer buffer) { + return readStr(buffer, CharsetUtil.CHARSET_UTF_8); + } + + /** + * 读取剩余部分并转为字符串 + * + * @param buffer ByteBuffer + * @param charset 编码 + * @return 字符串 + * @since 4.5.0 + */ + public static String readStr(ByteBuffer buffer, Charset charset) { + return StrUtil.str(readBytes(buffer), charset); + } + + /** + * 读取剩余部分bytes
+ * + * @param buffer ByteBuffer + * @return bytes + */ + public static byte[] readBytes(ByteBuffer buffer) { + final int remaining = buffer.remaining(); + byte[] ab = new byte[remaining]; + buffer.get(ab); + return ab; + } + + /** + * 读取指定长度的bytes
+ * 如果长度不足,则读取剩余部分,此时buffer必须为读模式 + * + * @param buffer ByteBuffer + * @param maxLength 最大长度 + * @return bytes + */ + public static byte[] readBytes(ByteBuffer buffer, int maxLength) { + final int remaining = buffer.remaining(); + if (maxLength > remaining) { + maxLength = remaining; + } + byte[] ab = new byte[maxLength]; + buffer.get(ab); + return ab; + } + + /** + * 读取指定区间的数据 + * + * @param buffer {@link ByteBuffer} + * @param start 开始位置 + * @param end 结束位置 + * @return bytes + */ + public static byte[] readBytes(ByteBuffer buffer, int start, int end) { + byte[] bs = new byte[end - start]; + System.arraycopy(buffer.array(), start, bs, 0, bs.length); + return bs; + } + + /** + * 一行的末尾位置,查找位置时位移ByteBuffer到结束位置 + * + * @param buffer {@link ByteBuffer} + * @return 末尾位置,未找到或达到最大长度返回-1 + */ + public static int lineEnd(ByteBuffer buffer) { + return lineEnd(buffer, buffer.remaining()); + } + + /** + * 一行的末尾位置,查找位置时位移ByteBuffer到结束位置
+ * 支持的换行符如下: + * + *
+	 * 1. \r\n
+	 * 2. \n
+	 * 
+ * + * @param buffer {@link ByteBuffer} + * @param maxLength 读取最大长度 + * @return 末尾位置,未找到或达到最大长度返回-1 + */ + public static int lineEnd(ByteBuffer buffer, int maxLength) { + int primitivePosition = buffer.position(); + boolean canEnd = false; + int charIndex = primitivePosition; + byte b; + while (buffer.hasRemaining()) { + b = buffer.get(); + charIndex++; + if (b == StrUtil.C_CR) { + canEnd = true; + } else if (b == StrUtil.C_LF) { + return canEnd ? charIndex - 2 : charIndex - 1; + } else { + // 只有\r无法确认换行 + canEnd = false; + } + + if (charIndex - primitivePosition > maxLength) { + // 查找到尽头,未找到,还原位置 + buffer.position(primitivePosition); + throw new IndexOutOfBoundsException(StrUtil.format("Position is out of maxLength: {}", maxLength)); + } + } + + // 查找到buffer尽头,未找到,还原位置 + buffer.position(primitivePosition); + // 读到结束位置 + return -1; + } + + /** + * 读取一行,如果buffer中最后一部分并非完整一行,则返回null
+ * 支持的换行符如下: + * + *
+	 * 1. \r\n
+	 * 2. \n
+	 * 
+ * + * @param buffer ByteBuffer + * @param charset 编码 + * @return 一行 + */ + public static String readLine(ByteBuffer buffer, Charset charset) { + final int startPosition = buffer.position(); + final int endPosition = lineEnd(buffer); + + if (endPosition > startPosition) { + byte[] bs = readBytes(buffer, startPosition, endPosition); + return StrUtil.str(bs, charset); + } else if (endPosition == startPosition) { + return StrUtil.EMPTY; + } + + return null; + } + + /** + * 创建新Buffer + * + * @param data 数据 + * @return {@link ByteBuffer} + * @since 4.5.0 + */ + public static ByteBuffer create(byte[] data) { + return ByteBuffer.wrap(data); + } + + /** + * 从字符串创建新Buffer + * + * @param data 数据 + * @param charset 编码 + * @return {@link ByteBuffer} + * @since 4.5.0 + */ + public static ByteBuffer create(CharSequence data, Charset charset) { + return create(StrUtil.bytes(data, charset)); + } + + /** + * 从字符串创建新Buffer,使用UTF-8编码 + * + * @param data 数据 + * @return {@link ByteBuffer} + * @since 4.5.0 + */ + public static ByteBuffer createUtf8(CharSequence data) { + return create(StrUtil.utf8Bytes(data)); + } + + /** + * 创建{@link CharBuffer} + * + * @param capacity 容量 + * @return {@link CharBuffer} + * @since 5.5.7 + */ + public static CharBuffer createCharBuffer(int capacity) { + return CharBuffer.allocate(capacity); + } +} diff --git a/src/main/java/cn/hutool/core/io/CharsetDetector.java b/src/main/java/cn/hutool/core/io/CharsetDetector.java new file mode 100644 index 0000000..9623bab --- /dev/null +++ b/src/main/java/cn/hutool/core/io/CharsetDetector.java @@ -0,0 +1,114 @@ +package cn.hutool.core.io; + +import cn.hutool.core.convert.Convert; +import cn.hutool.core.util.ArrayUtil; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; +import java.nio.charset.CharacterCodingException; +import java.nio.charset.Charset; +import java.nio.charset.CharsetDecoder; + +/** + * 编码探测器 + * + * @author looly + * @since 5.4.7 + */ +public class CharsetDetector { + + /** + * 默认的参与测试的编码 + */ + private static final Charset[] DEFAULT_CHARSETS; + + static { + String[] names = { + "UTF-8", + "GBK", + "GB2312", + "GB18030", + "UTF-16BE", + "UTF-16LE", + "UTF-16", + "BIG5", + "UNICODE", + "US-ASCII"}; + DEFAULT_CHARSETS = Convert.convert(Charset[].class, names); + } + + /** + * 探测文件编码 + * + * @param file 文件 + * @param charsets 需要测试用的编码,null或空使用默认的编码数组 + * @return 编码 + * @since 5.6.7 + */ + public static Charset detect(File file, Charset... charsets) { + return detect(FileUtil.getInputStream(file), charsets); + } + + /** + * 探测编码
+ * 注意:此方法会读取流的一部分,然后关闭流,如重复使用流,请使用支持reset方法的流 + * + * @param in 流,使用后关闭此流 + * @param charsets 需要测试用的编码,null或空使用默认的编码数组 + * @return 编码 + */ + public static Charset detect(InputStream in, Charset... charsets) { + return detect(IoUtil.DEFAULT_LARGE_BUFFER_SIZE, in, charsets); + } + + /** + * 探测编码
+ * 注意:此方法会读取流的一部分,然后关闭流,如重复使用流,请使用支持reset方法的流 + * + * @param bufferSize 自定义缓存大小,即每次检查的长度 + * @param in 流,使用后关闭此流 + * @param charsets 需要测试用的编码,null或空使用默认的编码数组 + * @return 编码 + * @since 5.7.10 + */ + public static Charset detect(int bufferSize, InputStream in, Charset... charsets) { + if (ArrayUtil.isEmpty(charsets)) { + charsets = DEFAULT_CHARSETS; + } + + final byte[] buffer = new byte[bufferSize]; + try { + while (in.read(buffer) > -1) { + for (Charset charset : charsets) { + final CharsetDecoder decoder = charset.newDecoder(); + if (identify(buffer, decoder)) { + return charset; + } + } + } + } catch (IOException e) { + throw new IORuntimeException(e); + } finally { + IoUtil.close(in); + } + return null; + } + + /** + * 通过try的方式测试指定bytes是否可以被解码,从而判断是否为指定编码 + * + * @param bytes 测试的bytes + * @param decoder 解码器 + * @return 是否是指定编码 + */ + private static boolean identify(byte[] bytes, CharsetDecoder decoder) { + try { + decoder.decode(ByteBuffer.wrap(bytes)); + } catch (CharacterCodingException e) { + return false; + } + return true; + } +} diff --git a/src/main/java/cn/hutool/core/io/FastByteArrayOutputStream.java b/src/main/java/cn/hutool/core/io/FastByteArrayOutputStream.java new file mode 100644 index 0000000..68d78b4 --- /dev/null +++ b/src/main/java/cn/hutool/core/io/FastByteArrayOutputStream.java @@ -0,0 +1,123 @@ +package cn.hutool.core.io; + +import cn.hutool.core.util.CharsetUtil; +import cn.hutool.core.util.ObjectUtil; + +import java.io.IOException; +import java.io.OutputStream; +import java.nio.charset.Charset; + +/** + * 基于快速缓冲FastByteBuffer的OutputStream,随着数据的增长自动扩充缓冲区 + *

+ * 可以通过{@link #toByteArray()}和 {@link #toString()}来获取数据 + *

+ * {@link #close()}方法无任何效果,当流被关闭后不会抛出IOException + *

+ * 这种设计避免重新分配内存块而是分配新增的缓冲区,缓冲区不会被GC,数据也不会被拷贝到其他缓冲区。 + * + * @author biezhi + */ +public class FastByteArrayOutputStream extends OutputStream { + + private final FastByteBuffer buffer; + + /** + * 构造 + */ + public FastByteArrayOutputStream() { + this(1024); + } + + /** + * 构造 + * + * @param size 预估大小 + */ + public FastByteArrayOutputStream(int size) { + buffer = new FastByteBuffer(size); + } + + @Override + public void write(byte[] b, int off, int len) { + buffer.append(b, off, len); + } + + @Override + public void write(int b) { + buffer.append((byte) b); + } + + public int size() { + return buffer.size(); + } + + /** + * 此方法无任何效果,当流被关闭后不会抛出IOException + */ + @Override + public void close() { + // nop + } + + public void reset() { + buffer.reset(); + } + + /** + * 写出 + * @param out 输出流 + * @throws IORuntimeException IO异常 + */ + public void writeTo(OutputStream out) throws IORuntimeException { + final int index = buffer.index(); + if(index < 0){ + // 无数据写出 + return; + } + byte[] buf; + try { + for (int i = 0; i < index; i++) { + buf = buffer.array(i); + out.write(buf); + } + out.write(buffer.array(index), 0, buffer.offset()); + } catch (IOException e) { + throw new IORuntimeException(e); + } + } + + + /** + * 转为Byte数组 + * @return Byte数组 + */ + public byte[] toByteArray() { + return buffer.toArray(); + } + + @Override + public String toString() { + return toString(CharsetUtil.defaultCharset()); + } + + /** + * 转为字符串 + * @param charsetName 编码 + * @return 字符串 + */ + public String toString(String charsetName) { + return toString(CharsetUtil.charset(charsetName)); + } + + /** + * 转为字符串 + * @param charset 编码,null表示默认编码 + * @return 字符串 + */ + public String toString(Charset charset) { + return new String(toByteArray(), + ObjectUtil.defaultIfNull(charset, CharsetUtil.defaultCharset())); + } + +} diff --git a/src/main/java/cn/hutool/core/io/FastByteBuffer.java b/src/main/java/cn/hutool/core/io/FastByteBuffer.java new file mode 100644 index 0000000..d2a7540 --- /dev/null +++ b/src/main/java/cn/hutool/core/io/FastByteBuffer.java @@ -0,0 +1,288 @@ +package cn.hutool.core.io; + +/** + * 代码移植自blade
+ * 快速缓冲,将数据存放在缓冲集中,取代以往的单一数组 + * + * @author biezhi, looly + * @since 1.0 + */ +public class FastByteBuffer { + + /** + * 缓冲集 + */ + private byte[][] buffers = new byte[16][]; + /** + * 缓冲数 + */ + private int buffersCount; + /** + * 当前缓冲索引 + */ + private int currentBufferIndex = -1; + /** + * 当前缓冲 + */ + private byte[] currentBuffer; + /** + * 当前缓冲偏移量 + */ + private int offset; + /** + * 缓冲字节数 + */ + private int size; + + /** + * 一个缓冲区的最小字节数 + */ + private final int minChunkLen; + + public FastByteBuffer() { + this(1024); + } + + public FastByteBuffer(int size) { + if(size <= 0){ + size = 1024; + } + this.minChunkLen = Math.abs(size); + } + + /** + * 分配下一个缓冲区,不会小于1024 + * + * @param newSize 理想缓冲区字节数 + */ + private void needNewBuffer(int newSize) { + int delta = newSize - size; + int newBufferSize = Math.max(minChunkLen, delta); + + currentBufferIndex++; + currentBuffer = new byte[newBufferSize]; + offset = 0; + + // add buffer + if (currentBufferIndex >= buffers.length) { + int newLen = buffers.length << 1; + byte[][] newBuffers = new byte[newLen][]; + System.arraycopy(buffers, 0, newBuffers, 0, buffers.length); + buffers = newBuffers; + } + buffers[currentBufferIndex] = currentBuffer; + buffersCount++; + } + + /** + * 向快速缓冲加入数据 + * + * @param array 数据 + * @param off 偏移量 + * @param len 字节数 + * @return 快速缓冲自身 @see FastByteBuffer + */ + public FastByteBuffer append(byte[] array, int off, int len) { + int end = off + len; + if ((off < 0) || (len < 0) || (end > array.length)) { + throw new IndexOutOfBoundsException(); + } + if (len == 0) { + return this; + } + int newSize = size + len; + int remaining = len; + + if (currentBuffer != null) { + // first try to fill current buffer + int part = Math.min(remaining, currentBuffer.length - offset); + System.arraycopy(array, end - remaining, currentBuffer, offset, part); + remaining -= part; + offset += part; + size += part; + } + + if (remaining > 0) { + // still some data left + // ask for new buffer + needNewBuffer(newSize); + + // then copy remaining + // but this time we are sure that it will fit + int part = Math.min(remaining, currentBuffer.length - offset); + System.arraycopy(array, end - remaining, currentBuffer, offset, part); + offset += part; + size += part; + } + + return this; + } + + /** + * 向快速缓冲加入数据 + * + * @param array 数据 + * + * @return 快速缓冲自身 @see FastByteBuffer + */ + public FastByteBuffer append(byte[] array) { + return append(array, 0, array.length); + } + + /** + * 向快速缓冲加入一个字节 + * + * @param element 一个字节的数据 + * @return 快速缓冲自身 @see FastByteBuffer + */ + public FastByteBuffer append(byte element) { + if ((currentBuffer == null) || (offset == currentBuffer.length)) { + needNewBuffer(size + 1); + } + + currentBuffer[offset] = element; + offset++; + size++; + + return this; + } + + /** + * 将另一个快速缓冲加入到自身 + * + * @param buff 快速缓冲 + * @return 快速缓冲自身 @see FastByteBuffer + */ + public FastByteBuffer append(FastByteBuffer buff) { + if (buff.size == 0) { + return this; + } + for (int i = 0; i < buff.currentBufferIndex; i++) { + append(buff.buffers[i]); + } + append(buff.currentBuffer, 0, buff.offset); + return this; + } + + public int size() { + return size; + } + + public boolean isEmpty() { + return size == 0; + } + + /** + * 当前缓冲位于缓冲区的索引位 + * + * @return {@link #currentBufferIndex} + */ + public int index() { + return currentBufferIndex; + } + + public int offset() { + return offset; + } + + /** + * 根据索引位返回缓冲集中的缓冲 + * + * @param index 索引位 + * @return 缓冲 + */ + public byte[] array(int index) { + return buffers[index]; + } + + public void reset() { + size = 0; + offset = 0; + currentBufferIndex = -1; + currentBuffer = null; + buffersCount = 0; + } + + /** + * 返回快速缓冲中的数据 + * + * @return 快速缓冲中的数据 + */ + public byte[] toArray() { + int pos = 0; + byte[] array = new byte[size]; + + if (currentBufferIndex == -1) { + return array; + } + + for (int i = 0; i < currentBufferIndex; i++) { + int len = buffers[i].length; + System.arraycopy(buffers[i], 0, array, pos, len); + pos += len; + } + + System.arraycopy(buffers[currentBufferIndex], 0, array, pos, offset); + + return array; + } + + /** + * 返回快速缓冲中的数据 + * + * @param start 逻辑起始位置 + * @param len 逻辑字节长 + * @return 快速缓冲中的数据 + */ + public byte[] toArray(int start, int len) { + int remaining = len; + int pos = 0; + byte[] array = new byte[len]; + + if (len == 0) { + return array; + } + + int i = 0; + while (start >= buffers[i].length) { + start -= buffers[i].length; + i++; + } + + while (i < buffersCount) { + byte[] buf = buffers[i]; + int c = Math.min(buf.length - start, remaining); + System.arraycopy(buf, start, array, pos, c); + pos += c; + remaining -= c; + if (remaining == 0) { + break; + } + start = 0; + i++; + } + return array; + } + + /** + * 根据索引位返回一个字节 + * + * @param index 索引位 + * @return 一个字节 + */ + public byte get(int index) { + if ((index >= size) || (index < 0)) { + throw new IndexOutOfBoundsException(); + } + int ndx = 0; + while (true) { + byte[] b = buffers[ndx]; + if (index < b.length) { + return b[index]; + } + ndx++; + index -= b.length; + } + } + +} diff --git a/src/main/java/cn/hutool/core/io/FastStringWriter.java b/src/main/java/cn/hutool/core/io/FastStringWriter.java new file mode 100644 index 0000000..1545ab9 --- /dev/null +++ b/src/main/java/cn/hutool/core/io/FastStringWriter.java @@ -0,0 +1,90 @@ +package cn.hutool.core.io; + +import cn.hutool.core.text.StrBuilder; + +import java.io.Writer; + +/** + * 借助{@link StrBuilder} 提供快读的字符串写出,相比jdk的StringWriter非线程安全,速度更快。 + * + * @author looly + * @since 5.3.3 + */ +public final class FastStringWriter extends Writer { + + private final StrBuilder builder; + + /** + * 构造 + */ + public FastStringWriter() { + this(StrBuilder.DEFAULT_CAPACITY); + } + + /** + * 构造 + * + * @param initialSize 初始容量 + */ + public FastStringWriter(int initialSize) { + if (initialSize < 0) { + initialSize = StrBuilder.DEFAULT_CAPACITY; + } + this.builder = new StrBuilder(initialSize); + } + + + @Override + public void write(final int c) { + this.builder.append((char) c); + } + + + @Override + public void write(final String str) { + this.builder.append(str); + } + + + @Override + public void write(final String str, final int off, final int len) { + this.builder.append(str, off, off + len); + } + + + @Override + public void write(final char[] cbuf) { + this.builder.append(cbuf, 0, cbuf.length); + } + + + @Override + public void write(final char[] cbuf, final int off, final int len) { + if ((off < 0) || (off > cbuf.length) || (len < 0) || + ((off + len) > cbuf.length) || ((off + len) < 0)) { + throw new IndexOutOfBoundsException(); + } else if (len == 0) { + return; + } + this.builder.append(cbuf, off, len); + } + + + @Override + public void flush() { + // Nothing to be flushed + } + + + @Override + public void close() { + // Nothing to be closed + } + + + @Override + public String toString() { + return this.builder.toString(); + } + +} diff --git a/src/main/java/cn/hutool/core/io/FileMagicNumber.java b/src/main/java/cn/hutool/core/io/FileMagicNumber.java new file mode 100644 index 0000000..a481a43 --- /dev/null +++ b/src/main/java/cn/hutool/core/io/FileMagicNumber.java @@ -0,0 +1,1243 @@ +package cn.hutool.core.io; + +import cn.hutool.core.util.ArrayUtil; + +import java.math.BigInteger; +import java.util.Arrays; +import java.util.Objects; + +/** + * 文件类型魔数封装 + * + * @author CherryRum + * @since 5.8.12 + */ +public enum FileMagicNumber { + UNKNOWN(null, null) { + @Override + public boolean match(final byte[] bytes) { + return false; + } + }, + //image start--------------------------------------------- + JPEG("image/jpeg", "jpg") { + @Override + public boolean match(final byte[] bytes) { + return bytes.length > 2 + && Objects.equals(bytes[0], (byte) 0xff) + && Objects.equals(bytes[1], (byte) 0xd8) + && Objects.equals(bytes[2], (byte) 0xff); + } + }, + JXR("image/vnd.ms-photo", "jxr") { + @Override + public boolean match(final byte[] bytes) { + //file magic number https://www.iana.org/assignments/media-types/image/jxr + return bytes.length > 2 + && Objects.equals(bytes[0], (byte) 0x49) + && Objects.equals(bytes[1], (byte) 0x49) + && Objects.equals(bytes[2], (byte) 0xbc); + } + }, + APNG("image/apng", "apng") { + @Override + public boolean match(final byte[] bytes) { + final boolean b = bytes.length > 8 + && Objects.equals(bytes[0], (byte) 0x89) + && Objects.equals(bytes[1], (byte) 0x50) + && Objects.equals(bytes[2], (byte) 0x4e) + && Objects.equals(bytes[3], (byte) 0x47) + && Objects.equals(bytes[4], (byte) 0x0d) + && Objects.equals(bytes[5], (byte) 0x0a) + && Objects.equals(bytes[6], (byte) 0x1a) + && Objects.equals(bytes[7], (byte) 0x0a); + + if (b) { + int i = 8; + while (i < bytes.length) { + try { + final int dataLength = new BigInteger(1, Arrays.copyOfRange(bytes, i, i + 4)).intValue(); + i += 4; + final byte[] bytes1 = Arrays.copyOfRange(bytes, i, i + 4); + final String chunkType = new String(bytes1); + i += 4; + if (Objects.equals(chunkType, "IDAT") || Objects.equals(chunkType, "IEND")) { + return false; + } else if (Objects.equals(chunkType, "acTL")) { + return true; + } + i += dataLength + 4; + } catch (final Exception e) { + return false; + } + } + } + return false; + } + }, + PNG("image/png", "png") { + @Override + public boolean match(final byte[] bytes) { + return bytes.length > 3 + && Objects.equals(bytes[0], (byte) 0x89) + && Objects.equals(bytes[1], (byte) 0x50) + && Objects.equals(bytes[2], (byte) 0x4e) + && Objects.equals(bytes[3], (byte) 0x47); + } + }, + GIF("image/gif", "gif") { + @Override + public boolean match(final byte[] bytes) { + return bytes.length > 2 + && Objects.equals(bytes[0], (byte) 0x47) + && Objects.equals(bytes[1], (byte) 0x49) + && Objects.equals(bytes[2], (byte) 0x46); + } + }, + BMP("image/bmp", "bmp") { + @Override + public boolean match(final byte[] bytes) { + return bytes.length > 1 + && Objects.equals(bytes[0], (byte) 0x42) + && Objects.equals(bytes[1], (byte) 0x4d); + } + }, + TIFF("image/tiff", "tiff") { + @Override + public boolean match(final byte[] bytes) { + if (bytes.length < 4) { + return false; + } + final boolean flag1 = Objects.equals(bytes[0], (byte) 0x49) + && Objects.equals(bytes[1], (byte) 0x49) + && Objects.equals(bytes[2], (byte) 0x2a) + && Objects.equals(bytes[3], (byte) 0x00); + final boolean flag2 = (Objects.equals(bytes[0], (byte) 0x4d) + && Objects.equals(bytes[1], (byte) 0x4d) + && Objects.equals(bytes[2], (byte) 0x00) + && Objects.equals(bytes[3], (byte) 0x2a)); + return flag1 || flag2; + + } + }, + + DWG("image/vnd.dwg", "dwg") { + @Override + public boolean match(final byte[] bytes) { + return bytes.length > 10 + && Objects.equals(bytes[0], (byte) 0x41) + && Objects.equals(bytes[1], (byte) 0x43) + && Objects.equals(bytes[2], (byte) 0x31) + && Objects.equals(bytes[3], (byte) 0x30); + } + }, + + WEBP("image/webp", "webp") { + @Override + public boolean match(final byte[] bytes) { + return bytes.length > 11 + && Objects.equals(bytes[8], (byte) 0x57) + && Objects.equals(bytes[9], (byte) 0x45) + && Objects.equals(bytes[10], (byte) 0x42) + && Objects.equals(bytes[11], (byte) 0x50); + } + }, + PSD("image/vnd.adobe.photoshop", "psd") { + @Override + public boolean match(final byte[] bytes) { + return bytes.length > 3 + && Objects.equals(bytes[0], (byte) 0x38) + && Objects.equals(bytes[1], (byte) 0x42) + && Objects.equals(bytes[2], (byte) 0x50) + && Objects.equals(bytes[3], (byte) 0x53); + } + }, + ICO("image/x-icon", "ico") { + @Override + public boolean match(final byte[] bytes) { + return bytes.length > 3 + && Objects.equals(bytes[0], (byte) 0x00) + && Objects.equals(bytes[1], (byte) 0x00) + && Objects.equals(bytes[2], (byte) 0x01) + && Objects.equals(bytes[3], (byte) 0x00); + } + }, + XCF("image/x-xcf", "xcf") { + @Override + public boolean match(final byte[] bytes) { + return bytes.length > 9 + && Objects.equals(bytes[0], (byte) 0x67) + && Objects.equals(bytes[1], (byte) 0x69) + && Objects.equals(bytes[2], (byte) 0x6d) + && Objects.equals(bytes[3], (byte) 0x70) + && Objects.equals(bytes[4], (byte) 0x20) + && Objects.equals(bytes[5], (byte) 0x78) + && Objects.equals(bytes[6], (byte) 0x63) + && Objects.equals(bytes[7], (byte) 0x66) + && Objects.equals(bytes[8], (byte) 0x20) + && Objects.equals(bytes[9], (byte) 0x76); + } + }, + //image end----------------------------------------------- + + //audio start--------------------------------------------- + + WAV("audio/x-wav", "wav") { + @Override + public boolean match(final byte[] bytes) { + return bytes.length > 11 + && Objects.equals(bytes[0], (byte) 0x52) + && Objects.equals(bytes[1], (byte) 0x49) + && Objects.equals(bytes[2], (byte) 0x46) + && Objects.equals(bytes[3], (byte) 0x46) + && Objects.equals(bytes[8], (byte) 0x57) + && Objects.equals(bytes[9], (byte) 0x41) + && Objects.equals(bytes[10], (byte) 0x56) + && Objects.equals(bytes[11], (byte) 0x45); + } + }, + MIDI("audio/midi", "midi") { + @Override + public boolean match(final byte[] bytes) { + return bytes.length > 3 + && Objects.equals(bytes[0], (byte) 0x4d) + && Objects.equals(bytes[1], (byte) 0x54) + && Objects.equals(bytes[2], (byte) 0x68) + && Objects.equals(bytes[3], (byte) 0x64); + } + }, + MP3("audio/mpeg", "mp3") { + @Override + public boolean match(final byte[] bytes) { + if (bytes.length < 2) { + return false; + } + final boolean flag1 = Objects.equals(bytes[0], (byte) 0x49) && Objects.equals(bytes[1], (byte) 0x44) && Objects.equals(bytes[2], (byte) 0x33); + final boolean flag2 = Objects.equals(bytes[0], (byte) 0xFF) && Objects.equals(bytes[1], (byte) 0xFB); + final boolean flag3 = Objects.equals(bytes[0], (byte) 0xFF) && Objects.equals(bytes[1], (byte) 0xF3); + final boolean flag4 = Objects.equals(bytes[0], (byte) 0xFF) && Objects.equals(bytes[1], (byte) 0xF2); + return flag1 || flag2 || flag3 || flag4; + } + }, + OGG("audio/ogg", "ogg") { + @Override + public boolean match(final byte[] bytes) { + return bytes.length > 3 + && Objects.equals(bytes[0], (byte) 0x4f) + && Objects.equals(bytes[1], (byte) 0x67) + && Objects.equals(bytes[2], (byte) 0x67) + && Objects.equals(bytes[3], (byte) 0x53); + } + }, + FLAC("audio/x-flac", "flac") { + @Override + public boolean match(final byte[] bytes) { + return bytes.length > 3 + && Objects.equals(bytes[0], (byte) 0x66) + && Objects.equals(bytes[1], (byte) 0x4c) + && Objects.equals(bytes[2], (byte) 0x61) + && Objects.equals(bytes[3], (byte) 0x43); + } + }, + M4A("audio/mp4", "m4a") { + @Override + public boolean match(final byte[] bytes) { + return (bytes.length > 10 + && Objects.equals(bytes[4], (byte) 0x66) + && Objects.equals(bytes[5], (byte) 0x74) + && Objects.equals(bytes[6], (byte) 0x79) + && Objects.equals(bytes[7], (byte) 0x70) + && Objects.equals(bytes[8], (byte) 0x4d) + && Objects.equals(bytes[9], (byte) 0x34) + && Objects.equals(bytes[10], (byte) 0x41)) + || (Objects.equals(bytes[0], (byte) 0x4d) + && Objects.equals(bytes[1], (byte) 0x34) + && Objects.equals(bytes[2], (byte) 0x41) + && Objects.equals(bytes[3], (byte) 0x20)); + } + }, + AAC("audio/aac", "aac") { + @Override + public boolean match(final byte[] bytes) { + if (bytes.length < 1) { + return false; + } + final boolean flag1 = Objects.equals(bytes[0], (byte) 0xFF) && Objects.equals(bytes[1], (byte) 0xF1); + final boolean flag2 = Objects.equals(bytes[0], (byte) 0xFF) && Objects.equals(bytes[1], (byte) 0xF9); + return flag1 || flag2; + } + }, + AMR("audio/amr", "amr") { + @Override + public boolean match(final byte[] bytes) { + //single-channel + if (bytes.length < 11) { + return false; + } + final boolean flag1 = Objects.equals(bytes[0], (byte) 0x23) + && Objects.equals(bytes[1], (byte) 0x21) + && Objects.equals(bytes[2], (byte) 0x41) + && Objects.equals(bytes[3], (byte) 0x4d) + && Objects.equals(bytes[4], (byte) 0x52) + && Objects.equals(bytes[5], (byte) 0x0A); + //multi-channel: + final boolean flag2 = Objects.equals(bytes[0], (byte) 0x23) + && Objects.equals(bytes[1], (byte) 0x21) + && Objects.equals(bytes[2], (byte) 0x41) + && Objects.equals(bytes[3], (byte) 0x4d) + && Objects.equals(bytes[4], (byte) 0x52) + && Objects.equals(bytes[5], (byte) 0x5F) + && Objects.equals(bytes[6], (byte) 0x4d) + && Objects.equals(bytes[7], (byte) 0x43) + && Objects.equals(bytes[8], (byte) 0x31) + && Objects.equals(bytes[9], (byte) 0x2e) + && Objects.equals(bytes[10], (byte) 0x30) + && Objects.equals(bytes[11], (byte) 0x0a); + return flag1 || flag2; + } + }, + AC3("audio/ac3", "ac3") { + @Override + public boolean match(final byte[] bytes) { + return bytes.length > 2 + && Objects.equals(bytes[0], (byte) 0x0b) + && Objects.equals(bytes[1], (byte) 0x77); + } + }, + AIFF("audio/x-aiff", "aiff") { + @Override + public boolean match(final byte[] bytes) { + return bytes.length > 11 + && Objects.equals(bytes[0], (byte) 0x46) + && Objects.equals(bytes[1], (byte) 0x4f) + && Objects.equals(bytes[2], (byte) 0x52) + && Objects.equals(bytes[3], (byte) 0x4d) + && Objects.equals(bytes[8], (byte) 0x41) + && Objects.equals(bytes[9], (byte) 0x49) + && Objects.equals(bytes[10], (byte) 0x46) + && Objects.equals(bytes[11], (byte) 0x46); + } + }, + //audio end----------------------------------------------- + + //font start--------------------------------------------- + // The existing registration "application/font-woff" is deprecated in favor of "font/woff". + WOFF("font/woff", "woff") { + @Override + public boolean match(final byte[] bytes) { + final boolean flag1 = Objects.equals(bytes[0], (byte) 0x77) + && Objects.equals(bytes[1], (byte) 0x4f) + && Objects.equals(bytes[2], (byte) 0x46) + && Objects.equals(bytes[3], (byte) 0x46); + final boolean flag2 = Objects.equals(bytes[4], (byte) 0x00) + && Objects.equals(bytes[5], (byte) 0x01) + && Objects.equals(bytes[6], (byte) 0x00) + && Objects.equals(bytes[7], (byte) 0x00); + final boolean flag3 = Objects.equals(bytes[4], (byte) 0x4f) + && Objects.equals(bytes[5], (byte) 0x54) + && Objects.equals(bytes[6], (byte) 0x54) + && Objects.equals(bytes[7], (byte) 0x4f); + final boolean flag4 = Objects.equals(bytes[4], (byte) 0x74) + && Objects.equals(bytes[5], (byte) 0x72) + && Objects.equals(bytes[6], (byte) 0x75) + && Objects.equals(bytes[7], (byte) 0x65); + return bytes.length > 7 + && (flag1 && (flag2 || flag3 || flag4)); + } + }, + WOFF2("font/woff2", "woff2") { + @Override + public boolean match(final byte[] bytes) { + final boolean flag1 = Objects.equals(bytes[0], (byte) 0x77) + && Objects.equals(bytes[1], (byte) 0x4f) + && Objects.equals(bytes[2], (byte) 0x46) + && Objects.equals(bytes[3], (byte) 0x32); + final boolean flag2 = Objects.equals(bytes[4], (byte) 0x00) + && Objects.equals(bytes[5], (byte) 0x01) + && Objects.equals(bytes[6], (byte) 0x00) + && Objects.equals(bytes[7], (byte) 0x00); + final boolean flag3 = Objects.equals(bytes[4], (byte) 0x4f) + && Objects.equals(bytes[5], (byte) 0x54) + && Objects.equals(bytes[6], (byte) 0x54) + && Objects.equals(bytes[7], (byte) 0x4f); + final boolean flag4 = Objects.equals(bytes[4], (byte) 0x74) + && Objects.equals(bytes[5], (byte) 0x72) + && Objects.equals(bytes[6], (byte) 0x75) + && Objects.equals(bytes[7], (byte) 0x65); + return bytes.length > 7 + && (flag1 && (flag2 || flag3 || flag4)); + } + }, + TTF("font/ttf", "ttf") { + @Override + public boolean match(final byte[] bytes) { + return bytes.length > 4 + && Objects.equals(bytes[0], (byte) 0x00) + && Objects.equals(bytes[1], (byte) 0x01) + && Objects.equals(bytes[2], (byte) 0x00) + && Objects.equals(bytes[3], (byte) 0x00) + && Objects.equals(bytes[4], (byte) 0x00); + } + }, + OTF("font/otf", "otf") { + @Override + public boolean match(final byte[] bytes) { + return bytes.length > 4 + && Objects.equals(bytes[0], (byte) 0x4f) + && Objects.equals(bytes[1], (byte) 0x54) + && Objects.equals(bytes[2], (byte) 0x54) + && Objects.equals(bytes[3], (byte) 0x4f) + && Objects.equals(bytes[4], (byte) 0x00); + } + }, + + //font end----------------------------------------------- + + //archive start----------------------------------------- + EPUB("application/epub+zip", "epub") { + @Override + public boolean match(final byte[] bytes) { + return bytes.length > 58 + && Objects.equals(bytes[0], (byte) 0x50) && Objects.equals(bytes[1], (byte) 0x4b) + && Objects.equals(bytes[2], (byte) 0x03) && Objects.equals(bytes[3], (byte) 0x04) + && Objects.equals(bytes[30], (byte) 0x6d) && Objects.equals(bytes[31], (byte) 0x69) + && Objects.equals(bytes[32], (byte) 0x6d) && Objects.equals(bytes[33], (byte) 0x65) + && Objects.equals(bytes[34], (byte) 0x74) && Objects.equals(bytes[35], (byte) 0x79) + && Objects.equals(bytes[36], (byte) 0x70) && Objects.equals(bytes[37], (byte) 0x65) + && Objects.equals(bytes[38], (byte) 0x61) && Objects.equals(bytes[39], (byte) 0x70) + && Objects.equals(bytes[40], (byte) 0x70) && Objects.equals(bytes[41], (byte) 0x6c) + && Objects.equals(bytes[42], (byte) 0x69) && Objects.equals(bytes[43], (byte) 0x63) + && Objects.equals(bytes[44], (byte) 0x61) && Objects.equals(bytes[45], (byte) 0x74) + && Objects.equals(bytes[46], (byte) 0x69) && Objects.equals(bytes[47], (byte) 0x6f) + && Objects.equals(bytes[48], (byte) 0x6e) && Objects.equals(bytes[49], (byte) 0x2f) + && Objects.equals(bytes[50], (byte) 0x65) && Objects.equals(bytes[51], (byte) 0x70) + && Objects.equals(bytes[52], (byte) 0x75) && Objects.equals(bytes[53], (byte) 0x62) + && Objects.equals(bytes[54], (byte) 0x2b) && Objects.equals(bytes[55], (byte) 0x7a) + && Objects.equals(bytes[56], (byte) 0x69) && Objects.equals(bytes[57], (byte) 0x70); + } + }, + ZIP("application/zip", "zip") { + @Override + public boolean match(final byte[] bytes) { + if (bytes.length < 4) { + return false; + } + final boolean flag1 = Objects.equals(bytes[0], (byte) 0x50) && Objects.equals(bytes[1], (byte) 0x4b); + final boolean flag2 = Objects.equals(bytes[2], (byte) 0x03) || Objects.equals(bytes[2], (byte) 0x05) || Objects.equals(bytes[2], (byte) 0x07); + final boolean flag3 = Objects.equals(bytes[3], (byte) 0x04) || Objects.equals(bytes[3], (byte) 0x06) || Objects.equals(bytes[3], (byte) 0x08); + return flag1 && flag2 && flag3; + } + }, + TAR("application/x-tar", "tar") { + @Override + public boolean match(final byte[] bytes) { + return bytes.length > 261 + && Objects.equals(bytes[257], (byte) 0x75) + && Objects.equals(bytes[258], (byte) 0x73) + && Objects.equals(bytes[259], (byte) 0x74) + && Objects.equals(bytes[260], (byte) 0x61) + && Objects.equals(bytes[261], (byte) 0x72); + } + }, + RAR("application/x-rar-compressed", "rar") { + @Override + public boolean match(final byte[] bytes) { + return bytes.length > 6 + && Objects.equals(bytes[0], (byte) 0x52) + && Objects.equals(bytes[1], (byte) 0x61) + && Objects.equals(bytes[2], (byte) 0x72) + && Objects.equals(bytes[3], (byte) 0x21) + && Objects.equals(bytes[4], (byte) 0x1a) + && Objects.equals(bytes[5], (byte) 0x07) + && (Objects.equals(bytes[6], (byte) 0x00) || Objects.equals(bytes[6], (byte) 0x01)); + } + }, + GZ("application/gzip", "gz") { + @Override + public boolean match(final byte[] bytes) { + return bytes.length > 2 + && Objects.equals(bytes[0], (byte) 0x1f) + && Objects.equals(bytes[1], (byte) 0x8b) + && Objects.equals(bytes[2], (byte) 0x08); + } + }, + BZ2("application/x-bzip2", "bz2") { + @Override + public boolean match(final byte[] bytes) { + return bytes.length > 2 + && Objects.equals(bytes[0], (byte) 0x42) + && Objects.equals(bytes[1], (byte) 0x5a) + && Objects.equals(bytes[2], (byte) 0x68); + } + }, + SevenZ("application/x-7z-compressed", "7z") { + @Override + public boolean match(final byte[] bytes) { + return bytes.length > 6 + && Objects.equals(bytes[0], (byte) 0x37) + && Objects.equals(bytes[1], (byte) 0x7a) + && Objects.equals(bytes[2], (byte) 0xbc) + && Objects.equals(bytes[3], (byte) 0xaf) + && Objects.equals(bytes[4], (byte) 0x27) + && Objects.equals(bytes[5], (byte) 0x1c) + && Objects.equals(bytes[6], (byte) 0x00); + } + }, + PDF("application/pdf", "pdf") { + @Override + public boolean match(byte[] bytes) { + //去除bom头并且跳过三个字节 + if (bytes.length > 3 && Objects.equals(bytes[0], (byte) 0xEF) + && Objects.equals(bytes[1], (byte) 0xBB) && Objects.equals(bytes[2], (byte) 0xBF)) { + bytes = Arrays.copyOfRange(bytes, 3, bytes.length); + } + return bytes.length > 3 + && Objects.equals(bytes[0], (byte) 0x25) + && Objects.equals(bytes[1], (byte) 0x50) + && Objects.equals(bytes[2], (byte) 0x44) + && Objects.equals(bytes[3], (byte) 0x46); + } + }, + EXE("application/x-msdownload", "exe") { + @Override + public boolean match(final byte[] bytes) { + return bytes.length > 1 + && Objects.equals(bytes[0], (byte) 0x4d) + && Objects.equals(bytes[1], (byte) 0x5a); + } + }, + SWF("application/x-shockwave-flash", "swf") { + @Override + public boolean match(final byte[] bytes) { + return bytes.length > 2 + && (Objects.equals(bytes[0], 0x43) || Objects.equals(bytes[0], (byte) 0x46)) + && Objects.equals(bytes[1], (byte) 0x57) + && Objects.equals(bytes[2], (byte) 0x53); + } + }, + RTF("application/rtf", "rtf") { + @Override + public boolean match(final byte[] bytes) { + return bytes.length > 4 + && Objects.equals(bytes[0], (byte) 0x7b) + && Objects.equals(bytes[1], (byte) 0x5c) + && Objects.equals(bytes[2], (byte) 0x72) + && Objects.equals(bytes[3], (byte) 0x74) + && Objects.equals(bytes[4], (byte) 0x66); + } + }, + NES("application/x-nintendo-nes-rom", "nes") { + @Override + public boolean match(final byte[] bytes) { + return bytes.length > 3 + && Objects.equals(bytes[0], (byte) 0x4e) + && Objects.equals(bytes[1], (byte) 0x45) + && Objects.equals(bytes[2], (byte) 0x53) + && Objects.equals(bytes[3], (byte) 0x1a); + } + }, + CRX("application/x-google-chrome-extension", "crx") { + @Override + public boolean match(final byte[] bytes) { + return bytes.length > 3 + && Objects.equals(bytes[0], (byte) 0x43) + && Objects.equals(bytes[1], (byte) 0x72) + && Objects.equals(bytes[2], (byte) 0x32) + && Objects.equals(bytes[3], (byte) 0x34); + } + }, + CAB("application/vnd.ms-cab-compressed", "cab") { + @Override + public boolean match(final byte[] bytes) { + if (bytes.length < 4) { + return false; + } + final boolean flag1 = Objects.equals(bytes[0], (byte) 0x4d) && Objects.equals(bytes[1], (byte) 0x53) + && Objects.equals(bytes[2], (byte) 0x43) && Objects.equals(bytes[3], (byte) 0x46); + final boolean flag2 = Objects.equals(bytes[0], (byte) 0x49) && Objects.equals(bytes[1], (byte) 0x53) + && Objects.equals(bytes[2], (byte) 0x63) && Objects.equals(bytes[3], (byte) 0x28); + return flag1 || flag2; + } + }, + PS("application/postscript", "ps") { + @Override + public boolean match(final byte[] bytes) { + return bytes.length > 1 + && Objects.equals(bytes[0], (byte) 0x25) + && Objects.equals(bytes[1], (byte) 0x21); + } + }, + XZ("application/x-xz", "xz") { + @Override + public boolean match(final byte[] bytes) { + return bytes.length > 5 + && Objects.equals(bytes[0], (byte) 0xFD) + && Objects.equals(bytes[1], (byte) 0x37) + && Objects.equals(bytes[2], (byte) 0x7A) + && Objects.equals(bytes[3], (byte) 0x58) + && Objects.equals(bytes[4], (byte) 0x5A) + && Objects.equals(bytes[5], (byte) 0x00); + } + }, + SQLITE("application/x-sqlite3", "sqlite") { + @Override + public boolean match(final byte[] bytes) { + return bytes.length > 15 + && Objects.equals(bytes[0], (byte) 0x53) && Objects.equals(bytes[1], (byte) 0x51) + && Objects.equals(bytes[2], (byte) 0x4c) && Objects.equals(bytes[3], (byte) 0x69) + && Objects.equals(bytes[4], (byte) 0x74) && Objects.equals(bytes[5], (byte) 0x65) + && Objects.equals(bytes[6], (byte) 0x20) && Objects.equals(bytes[7], (byte) 0x66) + && Objects.equals(bytes[8], (byte) 0x6f) && Objects.equals(bytes[9], (byte) 0x72) + && Objects.equals(bytes[10], (byte) 0x6d) && Objects.equals(bytes[11], (byte) 0x61) + && Objects.equals(bytes[12], (byte) 0x74) && Objects.equals(bytes[13], (byte) 0x20) + && Objects.equals(bytes[14], (byte) 0x33) && Objects.equals(bytes[15], (byte) 0x00); + } + }, + DEB("application/x-deb", "deb") { + @Override + public boolean match(final byte[] bytes) { + return bytes.length > 20 + && Objects.equals(bytes[0], (byte) 0x21) && Objects.equals(bytes[1], (byte) 0x3c) + && Objects.equals(bytes[2], (byte) 0x61) && Objects.equals(bytes[3], (byte) 0x72) + && Objects.equals(bytes[4], (byte) 0x63) && Objects.equals(bytes[5], (byte) 0x68) + && Objects.equals(bytes[6], (byte) 0x3e) && Objects.equals(bytes[7], (byte) 0x0a) + && Objects.equals(bytes[8], (byte) 0x64) && Objects.equals(bytes[9], (byte) 0x65) + && Objects.equals(bytes[10], (byte) 0x62) && Objects.equals(bytes[11], (byte) 0x69) + && Objects.equals(bytes[12], (byte) 0x61) && Objects.equals(bytes[13], (byte) 0x6e) + && Objects.equals(bytes[14], (byte) 0x2d) && Objects.equals(bytes[15], (byte) 0x62) + && Objects.equals(bytes[16], (byte) 0x69) && Objects.equals(bytes[17], (byte) 0x6e) + && Objects.equals(bytes[18], (byte) 0x61) && Objects.equals(bytes[19], (byte) 0x72) + && Objects.equals(bytes[20], (byte) 0x79); + } + }, + AR("application/x-unix-archive", "ar") { + @Override + public boolean match(final byte[] bytes) { + return bytes.length > 6 + && Objects.equals(bytes[0], (byte) 0x21) + && Objects.equals(bytes[1], (byte) 0x3c) + && Objects.equals(bytes[2], (byte) 0x61) + && Objects.equals(bytes[3], (byte) 0x72) + && Objects.equals(bytes[4], (byte) 0x63) + && Objects.equals(bytes[5], (byte) 0x68) + && Objects.equals(bytes[6], (byte) 0x3e); + } + }, + LZOP("application/x-lzop", "lzo") { + @Override + public boolean match(final byte[] bytes) { + return bytes.length > 7 + && Objects.equals(bytes[0], (byte) 0x89) + && Objects.equals(bytes[1], (byte) 0x4c) + && Objects.equals(bytes[2], (byte) 0x5a) + && Objects.equals(bytes[3], (byte) 0x4f) + && Objects.equals(bytes[4], (byte) 0x00) + && Objects.equals(bytes[5], (byte) 0x0d) + && Objects.equals(bytes[6], (byte) 0x0a) + && Objects.equals(bytes[7], (byte) 0x1a); + } + }, + LZ("application/x-lzip", "lz") { + @Override + public boolean match(final byte[] bytes) { + return bytes.length > 3 + && Objects.equals(bytes[0], (byte) 0x4c) + && Objects.equals(bytes[1], (byte) 0x5a) + && Objects.equals(bytes[2], (byte) 0x49) + && Objects.equals(bytes[3], (byte) 0x50); + } + }, + ELF("application/x-executable", "elf") { + @Override + public boolean match(final byte[] bytes) { + return bytes.length > 52 + && Objects.equals(bytes[0], (byte) 0x7f) + && Objects.equals(bytes[1], (byte) 0x45) + && Objects.equals(bytes[2], (byte) 0x4c) + && Objects.equals(bytes[3], (byte) 0x46); + } + }, + LZ4("application/x-lz4", "lz4") { + @Override + public boolean match(final byte[] bytes) { + return bytes.length > 4 + && Objects.equals(bytes[0], (byte) 0x04) + && Objects.equals(bytes[1], (byte) 0x22) + && Objects.equals(bytes[2], (byte) 0x4d) + && Objects.equals(bytes[3], (byte) 0x18); + } + }, + //https://github.com/madler/brotli/blob/master/br-format-v3.txt,brotli 没有固定的file magic number,所以此处只是参考 + BR("application/x-brotli", "br") { + @Override + public boolean match(final byte[] bytes) { + return bytes.length > 3 + && Objects.equals(bytes[0], (byte) 0xce) + && Objects.equals(bytes[1], (byte) 0xb2) + && Objects.equals(bytes[2], (byte) 0xcf) + && Objects.equals(bytes[3], (byte) 0x81); + } + }, + DCM("application/x-dicom", "dcm") { + @Override + public boolean match(final byte[] bytes) { + return bytes.length > 128 + && Objects.equals(bytes[128], (byte) 0x44) + && Objects.equals(bytes[129], (byte) 0x49) + && Objects.equals(bytes[130], (byte) 0x43) + && Objects.equals(bytes[131], (byte) 0x4d); + } + }, + RPM("application/x-rpm", "rpm") { + @Override + public boolean match(final byte[] bytes) { + return bytes.length > 4 + && Objects.equals(bytes[0], (byte) 0xed) + && Objects.equals(bytes[1], (byte) 0xab) + && Objects.equals(bytes[2], (byte) 0xee) + && Objects.equals(bytes[3], (byte) 0xdb); + } + }, + ZSTD("application/x-zstd", "zst") { + @Override + public boolean match(final byte[] bytes) { + final int length = bytes.length; + if (length < 5) { + return false; + } + final byte[] buf1 = new byte[]{(byte) 0x22, (byte) 0x23, (byte) 0x24, (byte) 0x25, (byte) 0x26, (byte) 0x27, (byte) 0x28}; + final boolean flag1 = ArrayUtil.contains(buf1, bytes[0]) + && Objects.equals(bytes[1], (byte) 0xb5) + && Objects.equals(bytes[2], (byte) 0x2f) + && Objects.equals(bytes[3], (byte) 0xfd); + if (flag1) { + return true; + } + if ((bytes[0] & 0xF0) == 0x50) { + return bytes[1] == 0x2A && bytes[2] == 0x4D && bytes[3] == 0x18; + } + return false; + } + }, + //archive end------------------------------------------------------------ + //video start------------------------------------------------------------ + MP4("video/mp4", "mp4") { + @Override + public boolean match(final byte[] bytes) { + if (bytes.length < 13) { + return false; + } + final boolean flag1 = Objects.equals(bytes[4], (byte) 0x66) + && Objects.equals(bytes[5], (byte) 0x74) + && Objects.equals(bytes[6], (byte) 0x79) + && Objects.equals(bytes[7], (byte) 0x70) + && Objects.equals(bytes[8], (byte) 0x4d) + && Objects.equals(bytes[9], (byte) 0x53) + && Objects.equals(bytes[10], (byte) 0x4e) + && Objects.equals(bytes[11], (byte) 0x56); + final boolean flag2 = Objects.equals(bytes[4], (byte) 0x66) + && Objects.equals(bytes[5], (byte) 0x74) + && Objects.equals(bytes[6], (byte) 0x79) + && Objects.equals(bytes[7], (byte) 0x70) + && Objects.equals(bytes[8], (byte) 0x69) + && Objects.equals(bytes[9], (byte) 0x73) + && Objects.equals(bytes[10], (byte) 0x6f) + && Objects.equals(bytes[11], (byte) 0x6d); + return flag1 || flag2; + } + }, + AVI("video/x-msvideo", "avi") { + @Override + public boolean match(final byte[] bytes) { + return bytes.length > 11 + && Objects.equals(bytes[0], (byte) 0x52) + && Objects.equals(bytes[1], (byte) 0x49) + && Objects.equals(bytes[2], (byte) 0x46) + && Objects.equals(bytes[3], (byte) 0x46) + && Objects.equals(bytes[8], (byte) 0x41) + && Objects.equals(bytes[9], (byte) 0x56) + && Objects.equals(bytes[10], (byte) 0x49) + && Objects.equals(bytes[11], (byte) 0x20); + } + }, + WMV("video/x-ms-wmv", "wmv") { + @Override + public boolean match(final byte[] bytes) { + return bytes.length > 9 + && Objects.equals(bytes[0], (byte) 0x30) + && Objects.equals(bytes[1], (byte) 0x26) + && Objects.equals(bytes[2], (byte) 0xb2) + && Objects.equals(bytes[3], (byte) 0x75) + && Objects.equals(bytes[4], (byte) 0x8e) + && Objects.equals(bytes[5], (byte) 0x66) + && Objects.equals(bytes[6], (byte) 0xcf) + && Objects.equals(bytes[7], (byte) 0x11) + && Objects.equals(bytes[8], (byte) 0xa6) + && Objects.equals(bytes[9], (byte) 0xd9); + } + }, + M4V("video/x-m4v", "m4v") { + @Override + public boolean match(final byte[] bytes) { + if (bytes.length < 12) { + return false; + } + final boolean flag1 = Objects.equals(bytes[4], (byte) 0x66) + && Objects.equals(bytes[5], (byte) 0x74) + && Objects.equals(bytes[6], (byte) 0x79) + && Objects.equals(bytes[7], (byte) 0x70) + && Objects.equals(bytes[8], (byte) 0x4d) + && Objects.equals(bytes[9], (byte) 0x34) + && Objects.equals(bytes[10], (byte) 0x56) + && Objects.equals(bytes[11], (byte) 0x20); + final boolean flag2 = Objects.equals(bytes[4], (byte) 0x66) + && Objects.equals(bytes[5], (byte) 0x74) + && Objects.equals(bytes[6], (byte) 0x79) + && Objects.equals(bytes[7], (byte) 0x70) + && Objects.equals(bytes[8], (byte) 0x6d) + && Objects.equals(bytes[9], (byte) 0x70) + && Objects.equals(bytes[10], (byte) 0x34) + && Objects.equals(bytes[11], (byte) 0x32); + return flag1 || flag2; + } + }, + FLV("video/x-flv", "flv") { + @Override + public boolean match(final byte[] bytes) { + return bytes.length > 3 + && Objects.equals(bytes[0], (byte) 0x46) + && Objects.equals(bytes[1], (byte) 0x4c) + && Objects.equals(bytes[2], (byte) 0x56) + && Objects.equals(bytes[3], (byte) 0x01); + } + }, + MKV("video/x-matroska", "mkv") { + @Override + public boolean match(final byte[] bytes) { + //0x42 0x82 0x88 0x6d 0x61 0x74 0x72 0x6f 0x73 0x6b 0x61 + final boolean flag1 = bytes.length > 11 + && Objects.equals(bytes[0], (byte) 0x1a) + && Objects.equals(bytes[1], (byte) 0x45) + && Objects.equals(bytes[2], (byte) 0xdf) + && Objects.equals(bytes[3], (byte) 0xa3); + + if (flag1) { + //此处需要判断是否是'\x42\x82\x88matroska',算法类似kmp判断 + final byte[] bytes1 = {(byte) 0x42, (byte) 0x82, (byte) 0x88, (byte) 0x6d, (byte) 0x61, (byte) 0x74, (byte) 0x72, (byte) 0x6f, (byte) 0x73, (byte) 0x6b, (byte) 0x61}; + final int index = FileMagicNumber.indexOf(bytes, bytes1); + return index > 0; + } + return false; + } + }, + + WEBM("video/webm", "webm") { + @Override + public boolean match(final byte[] bytes) { + final boolean flag1 = bytes.length > 8 + && Objects.equals(bytes[0], (byte) 0x1a) + && Objects.equals(bytes[1], (byte) 0x45) + && Objects.equals(bytes[2], (byte) 0xdf) + && Objects.equals(bytes[3], (byte) 0xa3); + if (flag1) { + //此处需要判断是否是'\x42\x82\x88webm',算法类似kmp判断 + final byte[] bytes1 = {(byte) 0x42, (byte) 0x82, (byte) 0x88, (byte) 0x77, (byte) 0x65, (byte) 0x62, (byte) 0x6d}; + final int index = FileMagicNumber.indexOf(bytes, bytes1); + return index > 0; + } + return false; + } + }, + //此文件签名非常复杂,只判断常见的几种 + MOV("video/quicktime", "mov") { + @Override + public boolean match(final byte[] bytes) { + if (bytes.length < 12) { + return false; + } + final boolean flag1 = Objects.equals(bytes[4], (byte) 0x66) + && Objects.equals(bytes[5], (byte) 0x74) + && Objects.equals(bytes[6], (byte) 0x79) + && Objects.equals(bytes[7], (byte) 0x70) + && Objects.equals(bytes[8], (byte) 0x71) + && Objects.equals(bytes[9], (byte) 0x74) + && Objects.equals(bytes[10], (byte) 0x20) + && Objects.equals(bytes[11], (byte) 0x20); + final boolean flag2 = Objects.equals(bytes[4], (byte) 0x6D) + && Objects.equals(bytes[5], (byte) 0x6F) + && Objects.equals(bytes[6], (byte) 0x6F) + && Objects.equals(bytes[7], (byte) 0x76); + final boolean flag3 = Objects.equals(bytes[4], (byte) 0x66) + && Objects.equals(bytes[5], (byte) 0x72) + && Objects.equals(bytes[6], (byte) 0x65) + && Objects.equals(bytes[7], (byte) 0x65); + final boolean flag4 = Objects.equals(bytes[4], (byte) 0x6D) + && Objects.equals(bytes[5], (byte) 0x64) + && Objects.equals(bytes[6], (byte) 0x61) + && Objects.equals(bytes[7], (byte) 0x74); + final boolean flag5 = Objects.equals(bytes[4], (byte) 0x77) + && Objects.equals(bytes[5], (byte) 0x69) + && Objects.equals(bytes[6], (byte) 0x64) + && Objects.equals(bytes[7], (byte) 0x65); + final boolean flag6 = Objects.equals(bytes[4], (byte) 0x70) + && Objects.equals(bytes[5], (byte) 0x6E) + && Objects.equals(bytes[6], (byte) 0x6F) + && Objects.equals(bytes[7], (byte) 0x74); + final boolean flag7 = Objects.equals(bytes[4], (byte) 0x73) + && Objects.equals(bytes[5], (byte) 0x6B) + && Objects.equals(bytes[6], (byte) 0x69) + && Objects.equals(bytes[7], (byte) 0x70); + return flag1 || flag2 || flag3 || flag4 || flag5 || flag6 || flag7; + } + }, + MPEG("video/mpeg", "mpg") { + @Override + public boolean match(final byte[] bytes) { + return bytes.length > 3 + && Objects.equals(bytes[0], (byte) 0x00) + && Objects.equals(bytes[1], (byte) 0x00) + && Objects.equals(bytes[2], (byte) 0x01) + && (bytes[3] >= (byte) 0xb0 && bytes[3] <= (byte) 0xbf); + } + }, + RMVB("video/vnd.rn-realvideo", "rmvb") { + @Override + public boolean match(final byte[] bytes) { + return bytes.length > 4 + && Objects.equals(bytes[0], (byte) 0x2E) + && Objects.equals(bytes[1], (byte) 0x52) + && Objects.equals(bytes[2], (byte) 0x4D) + && Objects.equals(bytes[3], (byte) 0x46); + } + }, + M3GP("video/3gpp", "3gp") { + @Override + public boolean match(final byte[] bytes) { + return bytes.length > 10 + && Objects.equals(bytes[4], (byte) 0x66) + && Objects.equals(bytes[5], (byte) 0x74) + && Objects.equals(bytes[6], (byte) 0x79) + && Objects.equals(bytes[7], (byte) 0x70) + && Objects.equals(bytes[8], (byte) 0x33) + && Objects.equals(bytes[9], (byte) 0x67) + && Objects.equals(bytes[10], (byte) 0x70); + } + }, + //video end --------------------------------------------------------------- + //document start ---------------------------------------------------------- + DOC("application/msword", "doc") { + @Override + public boolean match(final byte[] bytes) { + final byte[] byte1 = new byte[]{(byte) 0xd0, (byte) 0xcf, (byte) 0x11, (byte) 0xe0, (byte) 0xa1, (byte) 0xb1, (byte) 0x1a, (byte) 0xe1}; + final boolean flag1 = bytes.length > 515 && Arrays.equals(Arrays.copyOfRange(bytes, 0, 8), byte1); + if (flag1) { + final byte[] byte2 = new byte[]{(byte) 0xec, (byte) 0xa5, (byte) 0xc1, (byte) 0x00}; + final boolean flag2 = Arrays.equals(Arrays.copyOfRange(bytes, 512, 516), byte2); + final byte[] byte3 = new byte[]{(byte) 0x00, (byte) 0x0a, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x4d, (byte) 0x53, (byte) 0x57, (byte) 0x6f, (byte) 0x72, (byte) 0x64 + , (byte) 0x44, (byte) 0x6f, (byte) 0x63, (byte) 0x00, (byte) 0x10, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x57, (byte) 0x6f, (byte) 0x72, (byte) 0x64, + (byte) 0x2e, (byte) 0x44, (byte) 0x6f, (byte) 0x63, (byte) 0x75, (byte) 0x6d, (byte) 0x65, (byte) 0x6e, (byte) 0x74, (byte) 0x2e, (byte) 0x38, (byte) 0x00, + (byte) 0xf4, (byte) 0x39, (byte) 0xb2, (byte) 0x71}; + final byte[] range = Arrays.copyOfRange(bytes, 2075, 2142); + final boolean flag3 = bytes.length > 2142 && FileMagicNumber.indexOf(range, byte3) > 0; + return flag2 || flag3; + } + return false; + } + }, + + XLS("application/vnd.ms-excel", "xls") { + @Override + public boolean match(final byte[] bytes) { + final byte[] byte1 = new byte[]{(byte) 0xd0, (byte) 0xcf, (byte) 0x11, (byte) 0xe0, (byte) 0xa1, (byte) 0xb1, (byte) 0x1a, (byte) 0xe1}; + final boolean flag1 = bytes.length > 520 && Arrays.equals(Arrays.copyOfRange(bytes, 0, 8), byte1); + if (flag1) { + final byte[] byte2 = new byte[]{(byte) 0xfd, (byte) 0xff, (byte) 0xff, (byte) 0xff}; + final boolean flag2 = Arrays.equals(Arrays.copyOfRange(bytes, 512, 516), byte2) && (bytes[518] == 0x00 || bytes[518] == 0x02); + final byte[] byte3 = new byte[]{(byte) 0x09, (byte) 0x08, (byte) 0x10, (byte) 0x00, (byte) 0x00, (byte) 0x06, (byte) 0x05, (byte) 0x00}; + final boolean flag3 = Arrays.equals(Arrays.copyOfRange(bytes, 512, 520), byte3); + final byte[] byte4 = new byte[]{(byte) 0xe2, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x5c, (byte) 0x00, (byte) 0x70, (byte) 0x00, (byte) 0x04, (byte) 0x00, (byte) 0x00, (byte) 0x43, (byte) 0x61, (byte) 0x6c, (byte) 0x63}; + final boolean flag4 = bytes.length > 2095 && Arrays.equals(Arrays.copyOfRange(bytes, 1568, 2095), byte4); + return flag2 || flag3 || flag4; + } + return false; + } + + }, + PPT("application/vnd.ms-powerpoint", "ppt") { + @Override + public boolean match(final byte[] bytes) { + final byte[] byte1 = new byte[]{(byte) 0xd0, (byte) 0xcf, (byte) 0x11, (byte) 0xe0, (byte) 0xa1, (byte) 0xb1, (byte) 0x1a, (byte) 0xe1}; + final boolean flag1 = bytes.length > 524 && Arrays.equals(Arrays.copyOfRange(bytes, 0, 8), byte1); + if (flag1) { + final byte[] byte2 = new byte[]{(byte) 0xa0, (byte) 0x46, (byte) 0x1d, (byte) 0xf0}; + final byte[] byteRange = Arrays.copyOfRange(bytes, 512, 516); + final boolean flag2 = Arrays.equals(byteRange, byte2); + final byte[] byte3 = new byte[]{(byte) 0x00, (byte) 0x6e, (byte) 0x1e, (byte) 0xf0}; + final boolean flag3 = Arrays.equals(byteRange, byte3); + final byte[] byte4 = new byte[]{(byte) 0x0f, (byte) 0x00, (byte) 0xe8, (byte) 0x03}; + final boolean flag4 = Arrays.equals(byteRange, byte4); + final byte[] byte5 = new byte[]{(byte) 0xfd, (byte) 0xff, (byte) 0xff, (byte) 0xff}; + final boolean flag5 = Arrays.equals(byteRange, byte5) && bytes[522] == 0x00 && bytes[523] == 0x00; + final byte[] byte6 = new byte[]{(byte) 0x00, (byte) 0xb9, (byte) 0x29, (byte) 0xe8, (byte) 0x11, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x4d, (byte) 0x53, (byte) 0x20, (byte) 0x50, (byte) 0x6f, (byte) 0x77, (byte) 0x65, (byte) 0x72, (byte) 0x50, (byte) + 0x6f, (byte) 0x69, (byte) 0x6e, (byte) 0x74, (byte) 0x20, (byte) 0x39, (byte) 0x37}; + final boolean flag6 = bytes.length > 2096 && Arrays.equals(Arrays.copyOfRange(bytes, 2072, 2096), byte6); + return flag2 || flag3 || flag4 || flag5 || flag6; + } + return false; + } + }, + DOCX("application/vnd.openxmlformats-officedocument.wordprocessingml.document", "docx") { + @Override + public boolean match(final byte[] bytes) { + return Objects.equals(FileMagicNumber.matchDocument(bytes), DOCX); + } + }, + PPTX("application/vnd.openxmlformats-officedocument.presentationml.presentation", "pptx") { + @Override + public boolean match(final byte[] bytes) { + return Objects.equals(FileMagicNumber.matchDocument(bytes), PPTX); + } + }, + XLSX("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", "xlsx") { + @Override + public boolean match(final byte[] bytes) { + return Objects.equals(FileMagicNumber.matchDocument(bytes), XLSX); + } + }, + + //document end ------------------------------------------------------------ + //other start ------------------------------------------------------------- + WASM("application/wasm", "wasm") { + @Override + public boolean match(final byte[] bytes) { + return bytes.length > 7 + && Objects.equals(bytes[0], (byte) 0x00) + && Objects.equals(bytes[1], (byte) 0x61) + && Objects.equals(bytes[2], (byte) 0x73) + && Objects.equals(bytes[3], (byte) 0x6D) + && Objects.equals(bytes[4], (byte) 0x01) + && Objects.equals(bytes[5], (byte) 0x00) + && Objects.equals(bytes[6], (byte) 0x00) + && Objects.equals(bytes[7], (byte) 0x00); + } + }, + // https://source.android.com/devices/tech/dalvik/dex-format#dex-file-magic + DEX("application/vnd.android.dex", "dex") { + @Override + public boolean match(final byte[] bytes) { + return bytes.length > 36 + && Objects.equals(bytes[0], (byte) 0x64) + && Objects.equals(bytes[1], (byte) 0x65) + && Objects.equals(bytes[2], (byte) 0x78) + && Objects.equals(bytes[3], (byte) 0x0A) + && Objects.equals(bytes[36], (byte) 0x70); + } + }, + DEY("application/vnd.android.dey", "dey") { + @Override + public boolean match(final byte[] bytes) { + return bytes.length > 100 + && Objects.equals(bytes[0], (byte) 0x64) + && Objects.equals(bytes[1], (byte) 0x65) + && Objects.equals(bytes[2], (byte) 0x79) + && Objects.equals(bytes[3], (byte) 0x0A) && + DEX.match(Arrays.copyOfRange(bytes, 40, 100)); + } + }, + EML("message/rfc822", "eml") { + @Override + public boolean match(final byte[] bytes) { + if (bytes.length < 8) { + return false; + } + final byte[] byte1 = new byte[]{(byte) 0x46, (byte) 0x72, (byte) 0x6F, (byte) 0x6D, (byte) 0x20, (byte) 0x20, (byte) 0x20}; + final byte[] byte2 = new byte[]{(byte) 0x46, (byte) 0x72, (byte) 0x6F, (byte) 0x6D, (byte) 0x20, (byte) 0x3F, (byte) 0x3F, (byte) 0x3F}; + final byte[] byte3 = new byte[]{(byte) 0x46, (byte) 0x72, (byte) 0x6F, (byte) 0x6D, (byte) 0x3A, (byte) 0x20}; + final byte[] byte4 = new byte[]{(byte) 0x52, (byte) 0x65, (byte) 0x74, (byte) 0x75, (byte) 0x72, (byte) 0x6E, (byte) 0x2D, (byte) 0x50, (byte) 0x61, (byte) 0x74, (byte) 0x68, (byte) 0x3A, (byte) 0x20}; + return Arrays.equals(Arrays.copyOfRange(bytes, 0, 7), byte1) + || Arrays.equals(Arrays.copyOfRange(bytes, 0, 8), byte2) + || Arrays.equals(Arrays.copyOfRange(bytes, 0, 6), byte3) + || bytes.length > 13 && Arrays.equals(Arrays.copyOfRange(bytes, 0, 13), byte4); + } + }, + MDB("application/vnd.ms-access", "mdb") { + @Override + public boolean match(final byte[] bytes) { + final byte[] byte1 = new byte[]{(byte) 0x00, (byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x53, (byte) 0x74, (byte) 0x61, (byte) 0x6E, (byte) 0x64, + (byte) 0x61, (byte) 0x72, (byte) 0x64, (byte) 0x20, (byte) 0x4A, (byte) 0x65, (byte) 0x74, (byte) 0x20, (byte) 0x44, (byte) 0x42}; + return bytes.length > 18 && Arrays.equals(Arrays.copyOfRange(bytes, 0, 18), byte1); + } + }, + //CHM 49 54 53 46 + CHM("application/vnd.ms-htmlhelp", "chm") { + @Override + public boolean match(final byte[] bytes) { + final byte[] byte1 = new byte[]{(byte) 0x49, (byte) 0x54, (byte) 0x53, (byte) 0x46}; + return bytes.length > 4 && Arrays.equals(Arrays.copyOfRange(bytes, 0, 4), byte1); + } + }, + //class CA FE BA BE + CLASS("application/java-vm", "class") { + @Override + public boolean match(final byte[] bytes) { + final byte[] byte1 = new byte[]{(byte) 0xCA, (byte) 0xFE, (byte) 0xBA, (byte) 0xBE}; + return bytes.length > 4 && Arrays.equals(Arrays.copyOfRange(bytes, 0, 4), byte1); + } + }, + //torrent 64 38 3A 61 6E 6E 6F 75 6E 63 65 + TORRENT("application/x-bittorrent", "torrent") { + @Override + public boolean match(final byte[] bytes) { + final byte[] byte1 = new byte[]{(byte) 0x64, (byte) 0x38, (byte) 0x3A, (byte) 0x61, (byte) 0x6E, (byte) 0x6E, (byte) 0x6F, (byte) 0x75, (byte) 0x6E, (byte) 0x63, (byte) 0x65}; + return bytes.length > 11 && Arrays.equals(Arrays.copyOfRange(bytes, 0, 11), byte1); + } + }, + WPD("application/vnd.wordperfect", "wpd") { + @Override + public boolean match(final byte[] bytes) { + final byte[] byte1 = new byte[]{(byte) 0xFF, (byte) 0x57, (byte) 0x50, (byte) 0x43}; + return bytes.length > 4 && Arrays.equals(Arrays.copyOfRange(bytes, 0, 4), byte1); + } + }, + DBX("", "dbx") { + @Override + public boolean match(final byte[] bytes) { + final byte[] byte1 = new byte[]{(byte) 0xCF, (byte) 0xAD, (byte) 0x12, (byte) 0xFE}; + return bytes.length > 4 && Arrays.equals(Arrays.copyOfRange(bytes, 0, 4), byte1); + } + }, + PST("application/vnd.ms-outlook-pst", "pst") { + @Override + public boolean match(final byte[] bytes) { + final byte[] byte1 = new byte[]{(byte) 0x21, (byte) 0x42, (byte) 0x44, (byte) 0x4E}; + return bytes.length > 4 && Arrays.equals(Arrays.copyOfRange(bytes, 0, 4), byte1); + } + }, + RAM("audio/x-pn-realaudio", "ram") { + @Override + public boolean match(final byte[] bytes) { + final byte[] byte1 = new byte[]{(byte) 0x2E, (byte) 0x72, (byte) 0x61, (byte) 0xFD, (byte) 0x00}; + return bytes.length > 5 && Arrays.equals(Arrays.copyOfRange(bytes, 0, 5), byte1); + } + } + //other end --------------------------------------------------------------- + ; + private final String mimeType; + private final String extension; + + FileMagicNumber(final String mimeType, final String extension) { + this.mimeType = mimeType; + this.extension = extension; + } + + public static FileMagicNumber getMagicNumber(final byte[] bytes) { + final FileMagicNumber number = Arrays.stream(values()) + .filter(fileMagicNumber -> fileMagicNumber.match(bytes)) + .findFirst() + .orElse(UNKNOWN); + if (number.equals(FileMagicNumber.ZIP)) { + final FileMagicNumber fn = FileMagicNumber.matchDocument(bytes); + return fn == UNKNOWN ? ZIP : fn; + } + return number; + } + + public String getMimeType() { + return mimeType; + } + + public String getExtension() { + return extension; + } + + private static int indexOf(final byte[] array, final byte[] target) { + if (array == null || target == null || array.length < target.length) { + return -1; + } + if (target.length == 0) { + return 0; + } else { + label1: + for (int i = 0; i < array.length - target.length + 1; ++i) { + for (int j = 0; j < target.length; ++j) { + if (array[i + j] != target[j]) { + continue label1; + } + } + return i; + } + return -1; + } + } + + //处理 Open XML 类型的文件 + private static boolean compareBytes(final byte[] buf, final byte[] slice, final int startOffset) { + final int sl = slice.length; + if (startOffset + sl > buf.length) { + return false; + } + final byte[] sub = Arrays.copyOfRange(buf, startOffset, startOffset + sl); + return Arrays.equals(sub, slice); + } + + private static FileMagicNumber matchOpenXmlMime(final byte[] bytes, final int offset) { + final byte[] word = new byte[]{'w', 'o', 'r', 'd', '/'}; + final byte[] ppt = new byte[]{'p', 'p', 't', '/'}; + final byte[] xl = new byte[]{'x', 'l', '/'}; + if (FileMagicNumber.compareBytes(bytes, word, offset)) { + return FileMagicNumber.DOCX; + } + if (FileMagicNumber.compareBytes(bytes, ppt, offset)) { + return FileMagicNumber.PPTX; + } + if (FileMagicNumber.compareBytes(bytes, xl, offset)) { + return FileMagicNumber.XLSX; + } + return FileMagicNumber.UNKNOWN; + } + + private static FileMagicNumber matchDocument(final byte[] bytes) { + final FileMagicNumber fileMagicNumber = FileMagicNumber.matchOpenXmlMime(bytes, (byte) 0x1e); + if (!fileMagicNumber.equals(UNKNOWN)) { + return fileMagicNumber; + } + final byte[] bytes1 = new byte[]{0x5B, 0x43, 0x6F, 0x6E, 0x74, 0x65, 0x6E, 0x74, 0x5F, 0x54, 0x79, 0x70, 0x65, 0x73, 0x5D, 0x2E, 0x78, 0x6D, 0x6C}; + final byte[] bytes2 = new byte[]{0x5F, 0x72, 0x65, 0x6C, 0x73, 0x2F, 0x2E, 0x72, 0x65, 0x6C, 0x73}; + final byte[] bytes3 = new byte[]{0x64, 0x6F, 0x63, 0x50, 0x72, 0x6F, 0x70, 0x73}; + final boolean flag1 = FileMagicNumber.compareBytes(bytes, bytes1, (byte) 0x1e); + final boolean flag2 = FileMagicNumber.compareBytes(bytes, bytes2, (byte) 0x1e); + final boolean flag3 = FileMagicNumber.compareBytes(bytes, bytes3, (byte) 0x1e); + if (!(flag1 || flag2 || flag3)) { + return UNKNOWN; + } + int index = 0; + for (int i = 0; i < 4; i++) { + index = searchSignature(bytes, index + 4, 6000); + if (index == -1) { + continue; + } + final FileMagicNumber fn = FileMagicNumber.matchOpenXmlMime(bytes, index + 30); + if (!fn.equals(UNKNOWN)) { + return fn; + } + } + return UNKNOWN; + } + + private static int searchSignature(final byte[] bytes, final int start, final int rangeNum) { + final byte[] signature = new byte[]{0x50, 0x4B, 0x03, 0x04}; + final int length = bytes.length; + int end = start + rangeNum; + if (end > length) { + end = length; + } + final int index = FileMagicNumber.indexOf(Arrays.copyOfRange(bytes, start, end), signature); + return (index == -1) + ? -1 + : (start + index); + } + + public abstract boolean match(byte[] bytes); + +} diff --git a/src/main/java/cn/hutool/core/io/FileTypeUtil.java b/src/main/java/cn/hutool/core/io/FileTypeUtil.java new file mode 100644 index 0000000..f7eaf87 --- /dev/null +++ b/src/main/java/cn/hutool/core/io/FileTypeUtil.java @@ -0,0 +1,244 @@ +package cn.hutool.core.io; + +import cn.hutool.core.util.HexUtil; +import cn.hutool.core.util.StrUtil; + +import java.io.*; +import java.util.Map; +import java.util.Map.Entry; +import java.util.concurrent.ConcurrentSkipListMap; + +/** + * 文件类型判断工具类 + * + *

此工具根据文件的前几位bytes猜测文件类型,对于文本、zip判断不准确,对于视频、图片类型判断准确

+ * + *

需要注意的是,xlsx、docx等Office2007格式,全部识别为zip,因为新版采用了OpenXML格式,这些格式本质上是XML文件打包为zip

+ * + * @author Looly + */ +public class FileTypeUtil { + + private static final Map FILE_TYPE_MAP = new ConcurrentSkipListMap<>(); + + /** + * 增加文件类型映射
+ * 如果已经存在将覆盖之前的映射 + * + * @param fileStreamHexHead 文件流头部Hex信息 + * @param extName 文件扩展名 + * @return 之前已经存在的文件扩展名 + */ + public static String putFileType(String fileStreamHexHead, String extName) { + return FILE_TYPE_MAP.put(fileStreamHexHead, extName); + } + + /** + * 移除文件类型映射 + * + * @param fileStreamHexHead 文件流头部Hex信息 + * @return 移除的文件扩展名 + */ + public static String removeFileType(String fileStreamHexHead) { + return FILE_TYPE_MAP.remove(fileStreamHexHead); + } + + /** + * 根据文件流的头部信息获得文件类型 + * + * @param fileStreamHexHead 文件流头部16进制字符串 + * @return 文件类型,未找到为{@code null} + */ + public static String getType(String fileStreamHexHead) { + for (Entry fileTypeEntry : FILE_TYPE_MAP.entrySet()) { + if (StrUtil.startWithIgnoreCase(fileStreamHexHead, fileTypeEntry.getKey())) { + return fileTypeEntry.getValue(); + } + } + byte[] bytes = (HexUtil.decodeHex(fileStreamHexHead)); + return FileMagicNumber.getMagicNumber(bytes).getExtension(); + } + + /** + * 根据文件流的头部信息获得文件类型 + * + * @param in 文件流 + * @param fileHeadSize 自定义读取文件头部的大小 + * @return 文件类型,未找到为{@code null} + */ + public static String getType(InputStream in,int fileHeadSize) throws IORuntimeException { + return getType((IoUtil.readHex(in, fileHeadSize,false))); + } + + /** + * 根据文件流的头部信息获得文件类型
+ * 注意此方法会读取头部一些bytes,造成此流接下来读取时缺少部分bytes
+ * 因此如果想服用此流,流需支持{@link InputStream#reset()}方法。 + * @param in {@link InputStream} + * @param isExact 是否精确匹配,如果为false,使用前64个bytes匹配,如果为true,使用前8192bytes匹配 + * @return 类型,文件的扩展名,未找到为{@code null} + * @throws IORuntimeException 读取流引起的异常 + */ + public static String getType(InputStream in,boolean isExact) throws IORuntimeException { + return isExact + ?getType(IoUtil.readHex8192Upper(in)) + :getType(IoUtil.readHex64Upper(in)); + } + + /** + * 根据文件流的头部信息获得文件类型
+ * 注意此方法会读取头部64个bytes,造成此流接下来读取时缺少部分bytes
+ * 因此如果想服用此流,流需支持{@link InputStream#reset()}方法。 + * @param in {@link InputStream} + * @return 类型,文件的扩展名,未找到为{@code null} + * @throws IORuntimeException 读取流引起的异常 + */ + public static String getType(InputStream in) throws IORuntimeException { + return getType(in,false); + } + + /** + * 根据文件流的头部信息获得文件类型 + * 注意此方法会读取头部64个bytes,造成此流接下来读取时缺少部分bytes
+ * 因此如果想服用此流,流需支持{@link InputStream#reset()}方法。 + * + *
+	 *     1、无法识别类型默认按照扩展名识别
+	 *     2、xls、doc、msi头信息无法区分,按照扩展名区分
+	 *     3、zip可能为docx、xlsx、pptx、jar、war、ofd头信息无法区分,按照扩展名区分
+	 * 
+ * + * @param in {@link InputStream} + * @param filename 文件名 + * @return 类型,文件的扩展名,未找到为{@code null} + * @throws IORuntimeException 读取流引起的异常 + */ + public static String getType(InputStream in, String filename) throws IORuntimeException { + return getType(in,filename,false); + } + + /** + * 根据文件流的头部信息获得文件类型 + * 注意此方法会读取头部一些bytes,造成此流接下来读取时缺少部分bytes
+ * 因此如果想服用此流,流需支持{@link InputStream#reset()}方法。 + * + *
+	 *     1、无法识别类型默认按照扩展名识别
+	 *     2、xls、doc、msi头信息无法区分,按照扩展名区分
+	 *     3、zip可能为docx、xlsx、pptx、jar、war、ofd头信息无法区分,按照扩展名区分
+	 * 
+ * @param in {@link InputStream} + * @param filename 文件名 + * @param isExact 是否精确匹配,如果为false,使用前64个bytes匹配,如果为true,使用前8192bytes匹配 + * @return 类型,文件的扩展名,未找到为{@code null} + * @throws IORuntimeException 读取流引起的异常 + */ + public static String getType(InputStream in, String filename,boolean isExact) throws IORuntimeException { + String typeName = getType(in,isExact); + if (null == typeName) { + // 未成功识别类型,扩展名辅助识别 + typeName = FileUtil.extName(filename); + } else if ("zip".equals(typeName)) { + // zip可能为docx、xlsx、pptx、jar、war、ofd等格式,扩展名辅助判断 + final String extName = FileUtil.extName(filename); + if ("docx".equalsIgnoreCase(extName)) { + typeName = "docx"; + } else if ("xlsx".equalsIgnoreCase(extName)) { + typeName = "xlsx"; + } else if ("pptx".equalsIgnoreCase(extName)) { + typeName = "pptx"; + } else if ("jar".equalsIgnoreCase(extName)) { + typeName = "jar"; + } else if ("war".equalsIgnoreCase(extName)) { + typeName = "war"; + } else if ("ofd".equalsIgnoreCase(extName)) { + typeName = "ofd"; + } else if ("apk".equalsIgnoreCase(extName)) { + typeName = "apk"; + } + } else if ("jar".equals(typeName)) { + // wps编辑过的.xlsx文件与.jar的开头相同,通过扩展名判断 + final String extName = FileUtil.extName(filename); + if ("xlsx".equalsIgnoreCase(extName)) { + typeName = "xlsx"; + } else if ("docx".equalsIgnoreCase(extName)) { + // issue#I47JGH + typeName = "docx"; + } else if ("pptx".equalsIgnoreCase(extName)) { + // issue#I5A0GO + typeName = "pptx"; + } else if ("zip".equalsIgnoreCase(extName)) { + typeName = "zip"; + } else if ("apk".equalsIgnoreCase(extName)) { + typeName = "apk"; + } + } + return typeName; + } + + /** + * 根据文件流的头部信息获得文件类型 + * + *
+	 *     1、无法识别类型默认按照扩展名识别
+	 *     2、xls、doc、msi头信息无法区分,按照扩展名区分
+	 *     3、zip可能为jar、war头信息无法区分,按照扩展名区分
+	 * 
+ * + * @param file 文件 {@link File} + * @param isExact 是否精确匹配,如果为false,使用前64个bytes匹配,如果为true,使用前8192bytes匹配 + * @return 类型,文件的扩展名,未找到为{@code null} + * @throws IORuntimeException 读取文件引起的异常 + */ + public static String getType(File file,boolean isExact) throws IORuntimeException { + FileInputStream in = null; + try { + in = IoUtil.toStream(file); + return getType(in, file.getName(),isExact); + } finally { + IoUtil.close(in); + } + } + + /** + * 根据文件流的头部信息获得文件类型 + * + *
+	 *     1、无法识别类型默认按照扩展名识别
+	 *     2、xls、doc、msi头信息无法区分,按照扩展名区分
+	 *     3、zip可能为jar、war头信息无法区分,按照扩展名区分
+	 * 
+ * + * @param file 文件 {@link File} + * @return 类型,文件的扩展名,未找到为{@code null} + * @throws IORuntimeException 读取文件引起的异常 + */ + public static String getType(File file) throws IORuntimeException { + return getType(file,false); + } + + /** + * 通过路径获得文件类型 + * + * @param path 路径,绝对路径或相对ClassPath的路径 + * @param isExact 是否精确匹配,如果为false,使用前64个bytes匹配,如果为true,使用前8192bytes匹配 + * @return 类型 + * @throws IORuntimeException 读取文件引起的异常 + */ + public static String getTypeByPath(String path,boolean isExact) throws IORuntimeException { + return getType(FileUtil.file(path),isExact); + } + + /** + * 通过路径获得文件类型 + * + * @param path 路径,绝对路径或相对ClassPath的路径 + * @return 类型 + * @throws IORuntimeException 读取文件引起的异常 + */ + public static String getTypeByPath(String path) throws IORuntimeException { + return getTypeByPath(path,false); + } + + +} diff --git a/src/main/java/cn/hutool/core/io/FileUtil.java b/src/main/java/cn/hutool/core/io/FileUtil.java new file mode 100644 index 0000000..13412c2 --- /dev/null +++ b/src/main/java/cn/hutool/core/io/FileUtil.java @@ -0,0 +1,3593 @@ +package cn.hutool.core.io; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.io.file.FileCopier; +import cn.hutool.core.io.file.FileMode; +import cn.hutool.core.io.file.FileNameUtil; +import cn.hutool.core.io.file.FileReader; +import cn.hutool.core.io.file.FileReader.ReaderHandler; +import cn.hutool.core.io.file.FileWriter; +import cn.hutool.core.io.file.LineSeparator; +import cn.hutool.core.io.file.PathUtil; +import cn.hutool.core.io.file.Tailer; +import cn.hutool.core.io.resource.ResourceUtil; +import cn.hutool.core.io.unit.DataSizeUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.thread.ThreadUtil; +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.CharUtil; +import cn.hutool.core.util.CharsetUtil; +import cn.hutool.core.util.ClassUtil; +import cn.hutool.core.util.ReUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.core.util.URLUtil; +import cn.hutool.core.util.ZipUtil; + +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileFilter; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.io.LineNumberReader; +import java.io.OutputStream; +import java.io.PrintWriter; +import java.io.RandomAccessFile; +import java.io.Reader; +import java.net.URI; +import java.net.URL; +import java.net.URLConnection; +import java.nio.charset.Charset; +import java.nio.file.DirectoryNotEmptyException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Date; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.function.Consumer; +import java.util.jar.JarFile; +import java.util.regex.Pattern; +import java.util.zip.CRC32; +import java.util.zip.Checksum; + +/** + * 文件工具类 + * + * @author looly + */ +public class FileUtil extends PathUtil { + + /** + * Class文件扩展名 + */ + public static final String CLASS_EXT = FileNameUtil.EXT_CLASS; + /** + * Jar文件扩展名 + */ + public static final String JAR_FILE_EXT = FileNameUtil.EXT_JAR; + /** + * 在Jar中的路径jar的扩展名形式 + */ + public static final String JAR_PATH_EXT = ".jar!"; + /** + * 当Path为文件形式时, path会加入一个表示文件的前缀 + */ + public static final String PATH_FILE_PRE = URLUtil.FILE_URL_PREFIX; + /** + * 文件路径分隔符
+ * 在Unix和Linux下 是{@code '/'}; 在Windows下是 {@code '\'} + */ + public static final String FILE_SEPARATOR = File.separator; + /** + * 多个PATH之间的分隔符
+ * 在Unix和Linux下 是{@code ':'}; 在Windows下是 {@code ';'} + */ + public static final String PATH_SEPARATOR = File.pathSeparator; + /** + * 绝对路径判断正则 + */ + private static final Pattern PATTERN_PATH_ABSOLUTE = Pattern.compile("^[a-zA-Z]:([/\\\\].*)?"); + + + /** + * 是否为Windows环境 + * + * @return 是否为Windows环境 + * @since 3.0.9 + */ + public static boolean isWindows() { + return FileNameUtil.WINDOWS_SEPARATOR == File.separatorChar; + } + + /** + * 列出指定路径下的目录和文件
+ * 给定的绝对路径不能是压缩包中的路径 + * + * @param path 目录绝对路径或者相对路径 + * @return 文件列表(包含目录) + */ + public static File[] ls(String path) { + if (path == null) { + return null; + } + + File file = file(path); + if (file.isDirectory()) { + return file.listFiles(); + } + throw new IORuntimeException(StrUtil.format("Path [{}] is not directory!", path)); + } + + /** + * 文件是否为空
+ * 目录:里面没有文件时为空 文件:文件大小为0时为空 + * + * @param file 文件 + * @return 是否为空,当提供非目录时,返回false + */ + public static boolean isEmpty(File file) { + if (null == file || !file.exists()) { + return true; + } + + if (file.isDirectory()) { + String[] subFiles = file.list(); + return ArrayUtil.isEmpty(subFiles); + } else if (file.isFile()) { + return file.length() <= 0; + } + + return false; + } + + /** + * 目录是否为空 + * + * @param file 目录 + * @return 是否为空,当提供非目录时,返回false + */ + public static boolean isNotEmpty(File file) { + return !isEmpty(file); + } + + /** + * 目录是否为空 + * + * @param dir 目录 + * @return 是否为空 + */ + public static boolean isDirEmpty(File dir) { + return isDirEmpty(dir.toPath()); + } + + /** + * 递归遍历目录以及子目录中的所有文件
+ * 如果提供file为文件,直接返回过滤结果 + * + * @param path 当前遍历文件或目录的路径 + * @param fileFilter 文件过滤规则对象,选择要保留的文件,只对文件有效,不过滤目录 + * @return 文件列表 + * @since 3.2.0 + */ + public static List loopFiles(String path, FileFilter fileFilter) { + return loopFiles(file(path), fileFilter); + } + + /** + * 递归遍历目录以及子目录中的所有文件
+ * 如果提供file为文件,直接返回过滤结果 + * + * @param file 当前遍历文件或目录 + * @param fileFilter 文件过滤规则对象,选择要保留的文件,只对文件有效,不过滤目录 + * @return 文件列表 + */ + public static List loopFiles(File file, FileFilter fileFilter) { + return loopFiles(file, -1, fileFilter); + } + + /** + * 递归遍历目录并处理目录下的文件,可以处理目录或文件: + *
    + *
  • 非目录则直接调用{@link Consumer}处理
  • + *
  • 目录则递归调用此方法处理
  • + *
+ * + * @param file 文件或目录,文件直接处理 + * @param consumer 文件处理器,只会处理文件 + * @since 5.5.2 + */ + public static void walkFiles(File file, Consumer consumer) { + if (file.isDirectory()) { + final File[] subFiles = file.listFiles(); + if (ArrayUtil.isNotEmpty(subFiles)) { + for (File tmp : subFiles) { + walkFiles(tmp, consumer); + } + } + } else { + consumer.accept(file); + } + } + + /** + * 递归遍历目录以及子目录中的所有文件
+ * 如果提供file为文件,直接返回过滤结果 + * + * @param file 当前遍历文件或目录 + * @param maxDepth 遍历最大深度,-1表示遍历到没有目录为止 + * @param fileFilter 文件过滤规则对象,选择要保留的文件,只对文件有效,不过滤目录,null表示接收全部文件 + * @return 文件列表 + * @since 4.6.3 + */ + public static List loopFiles(File file, int maxDepth, FileFilter fileFilter) { + return loopFiles(file.toPath(), maxDepth, fileFilter); + } + + /** + * 递归遍历目录以及子目录中的所有文件
+ * 如果用户传入相对路径,则是相对classpath的路径
+ * 如:"test/aaa"表示"${classpath}/test/aaa" + * + * @param path 相对ClassPath的目录或者绝对路径目录 + * @return 文件列表 + * @since 3.2.0 + */ + public static List loopFiles(String path) { + return loopFiles(file(path)); + } + + /** + * 递归遍历目录以及子目录中的所有文件 + * + * @param file 当前遍历文件 + * @return 文件列表 + */ + public static List loopFiles(File file) { + return loopFiles(file, null); + } + + /** + * 获得指定目录下所有文件
+ * 不会扫描子目录
+ * 如果用户传入相对路径,则是相对classpath的路径
+ * 如:"test/aaa"表示"${classpath}/test/aaa" + * + * @param path 相对ClassPath的目录或者绝对路径目录 + * @return 文件路径列表(如果是jar中的文件,则给定类似.jar!/xxx/xxx的路径) + * @throws IORuntimeException IO异常 + */ + public static List listFileNames(String path) throws IORuntimeException { + if (path == null) { + return new ArrayList<>(0); + } + int index = path.lastIndexOf(FileUtil.JAR_PATH_EXT); + if (index < 0) { + // 普通目录 + final List paths = new ArrayList<>(); + final File[] files = ls(path); + for (File file : files) { + if (file.isFile()) { + paths.add(file.getName()); + } + } + return paths; + } + + // jar文件 + path = getAbsolutePath(path); + // jar文件中的路径 + index = index + FileUtil.JAR_FILE_EXT.length(); + JarFile jarFile = null; + try { + jarFile = new JarFile(path.substring(0, index)); + // 防止出现jar!/cn/hutool/这类路径导致文件找不到 + return ZipUtil.listFileNames(jarFile, StrUtil.removePrefix(path.substring(index + 1), "/")); + } catch (IOException e) { + throw new IORuntimeException(StrUtil.format("Can not read file path of [{}]", path), e); + } finally { + IoUtil.close(jarFile); + } + } + + /** + * 创建File对象,相当于调用new File(),不做任何处理 + * + * @param path 文件路径,相对路径表示相对项目路径 + * @return File + * @since 4.1.4 + */ + public static File newFile(String path) { + return new File(path); + } + + /** + * 创建File对象,自动识别相对或绝对路径,相对路径将自动从ClassPath下寻找 + * + * @param path 相对ClassPath的目录或者绝对路径目录 + * @return File + */ + public static File file(String path) { + if (null == path) { + return null; + } + return new File(getAbsolutePath(path)); + } + + /** + * 创建File对象
+ * 此方法会检查slip漏洞,漏洞说明见http://blog.nsfocus.net/zip-slip-2/ + * + * @param parent 父目录 + * @param path 文件路径 + * @return File + */ + public static File file(String parent, String path) { + return file(new File(parent), path); + } + + /** + * 创建File对象
+ * 根据的路径构建文件,在Win下直接构建,在Linux下拆分路径单独构建 + * 此方法会检查slip漏洞,漏洞说明见http://blog.nsfocus.net/zip-slip-2/ + * + * @param parent 父文件对象 + * @param path 文件路径 + * @return File + */ + public static File file(File parent, String path) { + if (StrUtil.isBlank(path)) { + throw new NullPointerException("File path is blank!"); + } + return checkSlip(parent, buildFile(parent, path)); + } + + /** + * 通过多层目录参数创建文件
+ * 此方法会检查slip漏洞,漏洞说明见http://blog.nsfocus.net/zip-slip-2/ + * + * @param directory 父目录 + * @param names 元素名(多层目录名),由外到内依次传入 + * @return the file 文件 + * @since 4.0.6 + */ + public static File file(File directory, String... names) { + Assert.notNull(directory, "directory must not be null"); + if (ArrayUtil.isEmpty(names)) { + return directory; + } + + File file = directory; + for (String name : names) { + if (null != name) { + file = file(file, name); + } + } + return file; + } + + /** + * 通过多层目录创建文件 + *

+ * 元素名(多层目录名) + * + * @param names 多层文件的文件名,由外到内依次传入 + * @return the file 文件 + * @since 4.0.6 + */ + public static File file(String... names) { + if (ArrayUtil.isEmpty(names)) { + return null; + } + + File file = null; + for (String name : names) { + if (file == null) { + file = file(name); + } else { + file = file(file, name); + } + } + return file; + } + + /** + * 创建File对象 + * + * @param uri 文件URI + * @return File + */ + public static File file(URI uri) { + if (uri == null) { + throw new NullPointerException("File uri is null!"); + } + return new File(uri); + } + + /** + * 创建File对象 + * + * @param url 文件URL + * @return File + */ + public static File file(URL url) { + return new File(URLUtil.toURI(url)); + } + + /** + * 获取临时文件路径(绝对路径) + * + * @return 临时文件路径 + * @since 4.0.6 + */ + public static String getTmpDirPath() { + return System.getProperty("java.io.tmpdir"); + } + + /** + * 获取临时文件目录 + * + * @return 临时文件目录 + * @since 4.0.6 + */ + public static File getTmpDir() { + return file(getTmpDirPath()); + } + + /** + * 获取用户路径(绝对路径) + * + * @return 用户路径 + * @since 4.0.6 + */ + public static String getUserHomePath() { + return System.getProperty("user.home"); + } + + /** + * 获取用户目录 + * + * @return 用户目录 + * @since 4.0.6 + */ + public static File getUserHomeDir() { + return file(getUserHomePath()); + } + + /** + * 判断文件是否存在,如果path为null,则返回false + * + * @param path 文件路径 + * @return 如果存在返回true + */ + public static boolean exist(String path) { + return (null != path) && file(path).exists(); + } + + /** + * 判断文件是否存在,如果file为null,则返回false + * + * @param file 文件 + * @return 如果存在返回true + */ + public static boolean exist(File file) { + return (null != file) && file.exists(); + } + + /** + * 是否存在匹配文件 + * + * @param directory 文件夹路径 + * @param regexp 文件夹中所包含文件名的正则表达式 + * @return 如果存在匹配文件返回true + */ + public static boolean exist(String directory, String regexp) { + final File file = new File(directory); + if (!file.exists()) { + return false; + } + + final String[] fileList = file.list(); + if (fileList == null) { + return false; + } + + for (String fileName : fileList) { + if (fileName.matches(regexp)) { + return true; + } + + } + return false; + } + + /** + * 指定文件最后修改时间 + * + * @param file 文件 + * @return 最后修改时间 + */ + public static Date lastModifiedTime(File file) { + if (!exist(file)) { + return null; + } + + return new Date(file.lastModified()); + } + + /** + * 指定路径文件最后修改时间 + * + * @param path 绝对路径 + * @return 最后修改时间 + */ + public static Date lastModifiedTime(String path) { + return lastModifiedTime(new File(path)); + } + + /** + * 计算目录或文件的总大小
+ * 当给定对象为文件时,直接调用 {@link File#length()}
+ * 当给定对象为目录时,遍历目录下的所有文件和目录,递归计算其大小,求和返回
+ * 此方法不包括目录本身的占用空间大小。 + * + * @param file 目录或文件,null或者文件不存在返回0 + * @return 总大小,bytes长度 + */ + public static long size(File file) { + return size(file, false); + } + + /** + * 计算目录或文件的总大小
+ * 当给定对象为文件时,直接调用 {@link File#length()}
+ * 当给定对象为目录时,遍历目录下的所有文件和目录,递归计算其大小,求和返回 + * + * @param file 目录或文件,null或者文件不存在返回0 + * @param includeDirSize 是否包括每层目录本身的大小 + * @return 总大小,bytes长度 + * @since 5.7.21 + */ + public static long size(File file, boolean includeDirSize) { + if (null == file || !file.exists() || isSymlink(file)) { + return 0; + } + + if (file.isDirectory()) { + long size = includeDirSize ? file.length() : 0; + File[] subFiles = file.listFiles(); + if (ArrayUtil.isEmpty(subFiles)) { + return 0L;// empty directory + } + for (File subFile : subFiles) { + size += size(subFile, includeDirSize); + } + return size; + } else { + return file.length(); + } + } + + /** + * 计算文件的总行数
+ * 读取文件采用系统默认编码,一般乱码不会造成行数错误。 + * + * @param file 文件 + * @return 该文件总行数 + * @since 5.7.22 + */ + public static int getTotalLines(File file) { + if (!isFile(file)) { + throw new IORuntimeException("Input must be a File"); + } + try (final LineNumberReader lineNumberReader = new LineNumberReader(new java.io.FileReader(file))) { + // 设置起始为1 + lineNumberReader.setLineNumber(1); + // 跳过文件中内容 + //noinspection ResultOfMethodCallIgnored + lineNumberReader.skip(Long.MAX_VALUE); + // 获取当前行号 + return lineNumberReader.getLineNumber(); + } catch (IOException e) { + throw new IORuntimeException(e); + } + } + + /** + * 给定文件或目录的最后修改时间是否晚于给定时间 + * + * @param file 文件或目录 + * @param reference 参照文件 + * @return 是否晚于给定时间 + */ + public static boolean newerThan(File file, File reference) { + if (null == reference || !reference.exists()) { + return true;// 文件一定比一个不存在的文件新 + } + return newerThan(file, reference.lastModified()); + } + + /** + * 给定文件或目录的最后修改时间是否晚于给定时间 + * + * @param file 文件或目录 + * @param timeMillis 做为对比的时间 + * @return 是否晚于给定时间 + */ + public static boolean newerThan(File file, long timeMillis) { + if (null == file || !file.exists()) { + return false;// 不存在的文件一定比任何时间旧 + } + return file.lastModified() > timeMillis; + } + + /** + * 创建文件及其父目录,如果这个文件存在,直接返回这个文件
+ * 此方法不对File对象类型做判断,如果File不存在,无法判断其类型 + * + * @param path 相对ClassPath的目录或者绝对路径目录,使用POSIX风格 + * @return 文件,若路径为null,返回null + * @throws IORuntimeException IO异常 + */ + public static File touch(String path) throws IORuntimeException { + if (path == null) { + return null; + } + return touch(file(path)); + } + + /** + * 创建文件及其父目录,如果这个文件存在,直接返回这个文件
+ * 此方法不对File对象类型做判断,如果File不存在,无法判断其类型 + * + * @param file 文件对象 + * @return 文件,若路径为null,返回null + * @throws IORuntimeException IO异常 + */ + public static File touch(File file) throws IORuntimeException { + if (null == file) { + return null; + } + if (!file.exists()) { + mkParentDirs(file); + try { + //noinspection ResultOfMethodCallIgnored + file.createNewFile(); + } catch (Exception e) { + throw new IORuntimeException(e); + } + } + return file; + } + + /** + * 创建文件及其父目录,如果这个文件存在,直接返回这个文件
+ * 此方法不对File对象类型做判断,如果File不存在,无法判断其类型 + * + * @param parent 父文件对象 + * @param path 文件路径 + * @return File + * @throws IORuntimeException IO异常 + */ + public static File touch(File parent, String path) throws IORuntimeException { + return touch(file(parent, path)); + } + + /** + * 创建文件及其父目录,如果这个文件存在,直接返回这个文件
+ * 此方法不对File对象类型做判断,如果File不存在,无法判断其类型 + * + * @param parent 父文件对象 + * @param path 文件路径 + * @return File + * @throws IORuntimeException IO异常 + */ + public static File touch(String parent, String path) throws IORuntimeException { + return touch(file(parent, path)); + } + + /** + * 创建所给文件或目录的父目录 + * + * @param file 文件或目录 + * @return 父目录 + */ + public static File mkParentDirs(File file) { + if (null == file) { + return null; + } + return mkdir(getParent(file, 1)); + } + + /** + * 创建父文件夹,如果存在直接返回此文件夹 + * + * @param path 文件夹路径,使用POSIX格式,无论哪个平台 + * @return 创建的目录 + */ + public static File mkParentDirs(String path) { + if (path == null) { + return null; + } + return mkParentDirs(file(path)); + } + + /** + * 删除文件或者文件夹
+ * 路径如果为相对路径,会转换为ClassPath路径! 注意:删除文件夹时不会判断文件夹是否为空,如果不空则递归删除子文件或文件夹
+ * 某个文件删除失败会终止删除操作 + * + * @param fullFileOrDirPath 文件或者目录的路径 + * @return 成功与否 + * @throws IORuntimeException IO异常 + */ + public static boolean del(String fullFileOrDirPath) throws IORuntimeException { + return del(file(fullFileOrDirPath)); + } + + /** + * 删除文件或者文件夹
+ * 注意:删除文件夹时不会判断文件夹是否为空,如果不空则递归删除子文件或文件夹
+ * 某个文件删除失败会终止删除操作 + * + *

+ * 从5.7.6开始,删除文件使用{@link Files#delete(Path)}代替 {@link File#delete()}
+ * 因为前者遇到文件被占用等原因时,抛出异常,而非返回false,异常会指明具体的失败原因。 + *

+ * + * @param file 文件对象 + * @return 成功与否 + * @throws IORuntimeException IO异常 + * @see Files#delete(Path) + */ + public static boolean del(File file) throws IORuntimeException { + if (file == null || !file.exists()) { + // 如果文件不存在或已被删除,此处返回true表示删除成功 + return true; + } + + if (file.isDirectory()) { + // 清空目录下所有文件和目录 + boolean isOk = clean(file); + if (!isOk) { + return false; + } + } + + // 删除文件或清空后的目录 + final Path path = file.toPath(); + try { + delFile(path); + } catch (DirectoryNotEmptyException e) { + // 遍历清空目录没有成功,此时补充删除一次(可能存在部分软链) + del(path); + } catch (IOException e) { + throw new IORuntimeException(e); + } + + return true; + } + + /** + * 清空文件夹
+ * 注意:清空文件夹时不会判断文件夹是否为空,如果不空则递归删除子文件或文件夹
+ * 某个文件删除失败会终止删除操作 + * + * @param dirPath 文件夹路径 + * @return 成功与否 + * @throws IORuntimeException IO异常 + * @since 4.0.8 + */ + public static boolean clean(String dirPath) throws IORuntimeException { + return clean(file(dirPath)); + } + + /** + * 清空文件夹
+ * 注意:清空文件夹时不会判断文件夹是否为空,如果不空则递归删除子文件或文件夹
+ * 某个文件删除失败会终止删除操作 + * + * @param directory 文件夹 + * @return 成功与否 + * @throws IORuntimeException IO异常 + * @since 3.0.6 + */ + public static boolean clean(File directory) throws IORuntimeException { + if (directory == null || !directory.exists() || !directory.isDirectory()) { + return true; + } + + final File[] files = directory.listFiles(); + if (null != files) { + for (File childFile : files) { + if (!del(childFile)) { + // 删除一个出错则本次删除任务失败 + return false; + } + } + } + return true; + } + + /** + * 清理空文件夹
+ * 此方法用于递归删除空的文件夹,不删除文件
+ * 如果传入的文件夹本身就是空的,删除这个文件夹 + * + * @param directory 文件夹 + * @return 成功与否 + * @throws IORuntimeException IO异常 + * @since 4.5.5 + */ + public static boolean cleanEmpty(File directory) throws IORuntimeException { + if (directory == null || !directory.exists() || !directory.isDirectory()) { + return true; + } + + final File[] files = directory.listFiles(); + if (ArrayUtil.isEmpty(files)) { + // 空文件夹则删除之 + return directory.delete(); + } + + for (File childFile : files) { + cleanEmpty(childFile); + } + return true; + } + + /** + * 创建文件夹,如果存在直接返回此文件夹
+ * 此方法不对File对象类型做判断,如果File不存在,无法判断其类型 + * + * @param dirPath 文件夹路径,使用POSIX格式,无论哪个平台 + * @return 创建的目录 + */ + public static File mkdir(String dirPath) { + if (dirPath == null) { + return null; + } + final File dir = file(dirPath); + return mkdir(dir); + } + + /** + * 创建文件夹,会递归自动创建其不存在的父文件夹,如果存在直接返回此文件夹
+ * 此方法不对File对象类型做判断,如果File不存在,无法判断其类型
+ * + * @param dir 目录 + * @return 创建的目录 + */ + public static File mkdir(File dir) { + if (dir == null) { + return null; + } + if (!dir.exists()) { + mkdirsSafely(dir, 5, 1); + } + return dir; + } + + /** + * 安全地级联创建目录 (确保并发环境下能创建成功) + * + *
+	 *     并发环境下,假设 test 目录不存在,如果线程A mkdirs "test/A" 目录,线程B mkdirs "test/B"目录,
+	 *     其中一个线程可能会失败,进而导致以下代码抛出 FileNotFoundException 异常
+	 *
+	 *     file.getParentFile().mkdirs(); // 父目录正在被另一个线程创建中,返回 false
+	 *     file.createNewFile(); // 抛出 IO 异常,因为该线程无法感知到父目录已被创建
+	 * 
+ * + * @param dir 待创建的目录 + * @param tryCount 最大尝试次数 + * @param sleepMillis 线程等待的毫秒数 + * @return true表示创建成功,false表示创建失败 + * @author z8g + * @since 5.7.21 + */ + public static boolean mkdirsSafely(File dir, int tryCount, long sleepMillis) { + if (dir == null) { + return false; + } + if (dir.isDirectory()) { + return true; + } + for (int i = 1; i <= tryCount; i++) { // 高并发场景下,可以看到 i 处于 1 ~ 3 之间 + // 如果文件已存在,也会返回 false,所以该值不能作为是否能创建的依据,因此不对其进行处理 + //noinspection ResultOfMethodCallIgnored + dir.mkdirs(); + if (dir.exists()) { + return true; + } + ThreadUtil.sleep(sleepMillis); + } + return dir.exists(); + } + + /** + * 创建临时文件
+ * 创建后的文件名为 prefix[Randon].tmp + * + * @param dir 临时文件创建的所在目录 + * @return 临时文件 + * @throws IORuntimeException IO异常 + */ + public static File createTempFile(File dir) throws IORuntimeException { + return createTempFile("hutool", null, dir, true); + } + + /** + * 在默认临时文件目录下创建临时文件,创建后的文件名为 prefix[Randon].tmp。 + * 默认临时文件目录由系统属性 {@code java.io.tmpdir} 指定。 + * 在 UNIX 系统上,此属性的默认值通常是 {@code "tmp"} 或 {@code "vartmp"}; + * 在 Microsoft Windows 系统上,它通常是 {@code "C:\\WINNT\\TEMP"}。 + * 调用 Java 虚拟机时,可以为该系统属性赋予不同的值,但不保证对该属性的编程更改对该方法使用的临时目录有任何影响。 + * + * @return 临时文件 + * @throws IORuntimeException IO异常 + * @since 5.7.22 + */ + public static File createTempFile() throws IORuntimeException { + return createTempFile("hutool", null, null, true); + } + + /** + * 在默认临时文件目录下创建临时文件,创建后的文件名为 prefix[Randon].suffix。 + * 默认临时文件目录由系统属性 {@code java.io.tmpdir} 指定。 + * 在 UNIX 系统上,此属性的默认值通常是 {@code "tmp"} 或 {@code "vartmp"}; + * 在 Microsoft Windows 系统上,它通常是 {@code "C:\\WINNT\\TEMP"}。 + * 调用 Java 虚拟机时,可以为该系统属性赋予不同的值,但不保证对该属性的编程更改对该方法使用的临时目录有任何影响。 + * + * @param suffix 后缀,如果null则使用默认.tmp + * @param isReCreat 是否重新创建文件(删掉原来的,创建新的) + * @return 临时文件 + * @throws IORuntimeException IO异常 + * @since 5.7.22 + */ + public static File createTempFile(String suffix, boolean isReCreat) throws IORuntimeException { + return createTempFile("hutool", suffix, null, isReCreat); + } + + /** + * 在默认临时文件目录下创建临时文件,创建后的文件名为 prefix[Randon].suffix。 + * 默认临时文件目录由系统属性 {@code java.io.tmpdir} 指定。 + * 在 UNIX 系统上,此属性的默认值通常是 {@code "tmp"} 或 {@code "vartmp"}; + * 在 Microsoft Windows 系统上,它通常是 {@code "C:\\WINNT\\TEMP"}。 + * 调用 Java 虚拟机时,可以为该系统属性赋予不同的值,但不保证对该属性的编程更改对该方法使用的临时目录有任何影响。 + * + * @param prefix 前缀,至少3个字符 + * @param suffix 后缀,如果null则使用默认.tmp + * @param isReCreat 是否重新创建文件(删掉原来的,创建新的) + * @return 临时文件 + * @throws IORuntimeException IO异常 + * @since 5.7.22 + */ + public static File createTempFile(String prefix, String suffix, boolean isReCreat) throws IORuntimeException { + return createTempFile(prefix, suffix, null, isReCreat); + } + + /** + * 创建临时文件
+ * 创建后的文件名为 prefix[Randon].tmp + * + * @param dir 临时文件创建的所在目录 + * @param isReCreat 是否重新创建文件(删掉原来的,创建新的) + * @return 临时文件 + * @throws IORuntimeException IO异常 + */ + public static File createTempFile(File dir, boolean isReCreat) throws IORuntimeException { + return createTempFile("hutool", null, dir, isReCreat); + } + + /** + * 创建临时文件
+ * 创建后的文件名为 prefix[Randon].suffix From com.jodd.io.FileUtil + * + * @param prefix 前缀,至少3个字符 + * @param suffix 后缀,如果null则使用默认.tmp + * @param dir 临时文件创建的所在目录 + * @param isReCreat 是否重新创建文件(删掉原来的,创建新的) + * @return 临时文件 + * @throws IORuntimeException IO异常 + */ + public static File createTempFile(String prefix, String suffix, File dir, boolean isReCreat) throws IORuntimeException { + int exceptionsCount = 0; + while (true) { + try { + File file = File.createTempFile(prefix, suffix, mkdir(dir)).getCanonicalFile(); + if (isReCreat) { + //noinspection ResultOfMethodCallIgnored + file.delete(); + //noinspection ResultOfMethodCallIgnored + file.createNewFile(); + } + return file; + } catch (IOException ioex) { // fixes java.io.WinNTFileSystem.createFileExclusively access denied + if (++exceptionsCount >= 50) { + throw new IORuntimeException(ioex); + } + } + } + } + + /** + * 通过JDK7+的 Files#copy(Path, Path, CopyOption...) 方法拷贝文件 + * + * @param src 源文件路径 + * @param dest 目标文件或目录路径,如果为目录使用与源文件相同的文件名 + * @param options {@link StandardCopyOption} + * @return File + * @throws IORuntimeException IO异常 + */ + public static File copyFile(String src, String dest, StandardCopyOption... options) throws IORuntimeException { + Assert.notBlank(src, "Source File path is blank !"); + Assert.notBlank(dest, "Destination File path is blank !"); + return copyFile(Paths.get(src), Paths.get(dest), options).toFile(); + } + + /** + * 通过JDK7+的 Files#copy(Path, Path, CopyOption...) 方法拷贝文件 + * + * @param src 源文件 + * @param dest 目标文件或目录,如果为目录使用与源文件相同的文件名 + * @param options {@link StandardCopyOption} + * @return 目标文件 + * @throws IORuntimeException IO异常 + */ + public static File copyFile(File src, File dest, StandardCopyOption... options) throws IORuntimeException { + // check + Assert.notNull(src, "Source File is null !"); + if (!src.exists()) { + throw new IORuntimeException("File not exist: " + src); + } + Assert.notNull(dest, "Destination File or directiory is null !"); + if (equals(src, dest)) { + throw new IORuntimeException("Files '{}' and '{}' are equal", src, dest); + } + return copyFile(src.toPath(), dest.toPath(), options).toFile(); + } + + /** + * 复制文件或目录
+ * 如果目标文件为目录,则将源文件以相同文件名拷贝到目标目录 + * + * @param srcPath 源文件或目录 + * @param destPath 目标文件或目录,目标不存在会自动创建(目录、文件都创建) + * @param isOverride 是否覆盖目标文件 + * @return 目标目录或文件 + * @throws IORuntimeException IO异常 + */ + public static File copy(String srcPath, String destPath, boolean isOverride) throws IORuntimeException { + return copy(file(srcPath), file(destPath), isOverride); + } + + /** + * 复制文件或目录
+ * 情况如下: + * + *
+	 * 1、src和dest都为目录,则将src目录及其目录下所有文件目录拷贝到dest下
+	 * 2、src和dest都为文件,直接复制,名字为dest
+	 * 3、src为文件,dest为目录,将src拷贝到dest目录下
+	 * 
+ * + * @param src 源文件 + * @param dest 目标文件或目录,目标不存在会自动创建(目录、文件都创建) + * @param isOverride 是否覆盖目标文件 + * @return 目标目录或文件 + * @throws IORuntimeException IO异常 + */ + public static File copy(File src, File dest, boolean isOverride) throws IORuntimeException { + return FileCopier.create(src, dest).setOverride(isOverride).copy(); + } + + /** + * 复制文件或目录
+ * 情况如下: + * + *
+	 * 1、src和dest都为目录,则将src下所有文件目录拷贝到dest下
+	 * 2、src和dest都为文件,直接复制,名字为dest
+	 * 3、src为文件,dest为目录,将src拷贝到dest目录下
+	 * 
+ * + * @param src 源文件 + * @param dest 目标文件或目录,目标不存在会自动创建(目录、文件都创建) + * @param isOverride 是否覆盖目标文件 + * @return 目标目录或文件 + * @throws IORuntimeException IO异常 + */ + public static File copyContent(File src, File dest, boolean isOverride) throws IORuntimeException { + return FileCopier.create(src, dest).setCopyContentIfDir(true).setOverride(isOverride).copy(); + } + + /** + * 复制文件或目录
+ * 情况如下: + * + *
+	 * 1、src和dest都为目录,则将src下所有文件(包括子目录)拷贝到dest下
+	 * 2、src和dest都为文件,直接复制,名字为dest
+	 * 3、src为文件,dest为目录,将src拷贝到dest目录下
+	 * 
+ * + * @param src 源文件 + * @param dest 目标文件或目录,目标不存在会自动创建(目录、文件都创建) + * @param isOverride 是否覆盖目标文件 + * @return 目标目录或文件 + * @throws IORuntimeException IO异常 + * @since 4.1.5 + */ + public static File copyFilesFromDir(File src, File dest, boolean isOverride) throws IORuntimeException { + return FileCopier.create(src, dest).setCopyContentIfDir(true).setOnlyCopyFile(true).setOverride(isOverride).copy(); + } + + /** + * 移动文件或者目录 + * + * @param src 源文件或者目录 + * @param target 目标文件或者目录 + * @param isOverride 是否覆盖目标,只有目标为文件才覆盖 + * @throws IORuntimeException IO异常 + * @see PathUtil#move(Path, Path, boolean) + */ + public static void move(File src, File target, boolean isOverride) throws IORuntimeException { + Assert.notNull(src, "Src file must be not null!"); + Assert.notNull(target, "target file must be not null!"); + move(src.toPath(), target.toPath(), isOverride); + } + + /** + * 移动文件或者目录 + * + * @param src 源文件或者目录 + * @param target 目标文件或者目录 + * @param isOverride 是否覆盖目标,只有目标为文件才覆盖 + * @throws IORuntimeException IO异常 + * @see PathUtil#moveContent(Path, Path, boolean) + * @since 5.7.9 + */ + public static void moveContent(File src, File target, boolean isOverride) throws IORuntimeException { + Assert.notNull(src, "Src file must be not null!"); + Assert.notNull(target, "target file must be not null!"); + moveContent(src.toPath(), target.toPath(), isOverride); + } + + /** + * 修改文件或目录的文件名,不变更路径,只是简单修改文件名,不保留扩展名。
+ * + *
+	 * FileUtil.rename(file, "aaa.png", true) xx/xx.png =》xx/aaa.png
+	 * 
+ * + * @param file 被修改的文件 + * @param newName 新的文件名,如需扩展名,需自行在此参数加上,原文件名的扩展名不会被保留 + * @param isOverride 是否覆盖目标文件 + * @return 目标文件 + * @since 5.3.6 + */ + public static File rename(File file, String newName, boolean isOverride) { + return rename(file, newName, false, isOverride); + } + + /** + * 修改文件或目录的文件名,不变更路径,只是简单修改文件名
+ * 重命名有两种模式:
+ * 1、isRetainExt为true时,保留原扩展名: + * + *
+	 * FileUtil.rename(file, "aaa", true) xx/xx.png =》xx/aaa.png
+	 * 
+ * + *

+ * 2、isRetainExt为false时,不保留原扩展名,需要在newName中 + * + *

+	 * FileUtil.rename(file, "aaa.jpg", false) xx/xx.png =》xx/aaa.jpg
+	 * 
+ * + * @param file 被修改的文件 + * @param newName 新的文件名,可选是否包括扩展名 + * @param isRetainExt 是否保留原文件的扩展名,如果保留,则newName不需要加扩展名 + * @param isOverride 是否覆盖目标文件 + * @return 目标文件 + * @see PathUtil#rename(Path, String, boolean) + * @since 3.0.9 + */ + public static File rename(File file, String newName, boolean isRetainExt, boolean isOverride) { + if (isRetainExt) { + final String extName = FileUtil.extName(file); + if (StrUtil.isNotBlank(extName)) { + newName = newName.concat(".").concat(extName); + } + } + return rename(file.toPath(), newName, isOverride).toFile(); + } + + /** + * 获取规范的绝对路径 + * + * @param file 文件 + * @return 规范绝对路径,如果传入file为null,返回null + * @since 4.1.4 + */ + public static String getCanonicalPath(File file) { + if (null == file) { + return null; + } + try { + return file.getCanonicalPath(); + } catch (IOException e) { + throw new IORuntimeException(e); + } + } + + /** + * 获取绝对路径
+ * 此方法不会判定给定路径是否有效(文件或目录存在) + * + * @param path 相对路径 + * @param baseClass 相对路径所相对的类 + * @return 绝对路径 + */ + public static String getAbsolutePath(String path, Class baseClass) { + String normalPath; + if (path == null) { + normalPath = StrUtil.EMPTY; + } else { + normalPath = normalize(path); + if (isAbsolutePath(normalPath)) { + // 给定的路径已经是绝对路径了 + return normalPath; + } + } + + // 相对于ClassPath路径 + final URL url = ResourceUtil.getResource(normalPath, baseClass); + if (null != url) { + // 对于jar中文件包含file:前缀,需要去掉此类前缀,在此做标准化,since 3.0.8 解决中文或空格路径被编码的问题 + return FileUtil.normalize(URLUtil.getDecodedPath(url)); + } + + // 如果资源不存在,则返回一个拼接的资源绝对路径 + final String classPath = ClassUtil.getClassPath(); + if (null == classPath) { + // throw new NullPointerException("ClassPath is null !"); + // 在jar运行模式中,ClassPath有可能获取不到,此时返回原始相对路径(此时获取的文件为相对工作目录) + return path; + } + + // 资源不存在的情况下使用标准化路径有问题,使用原始路径拼接后标准化路径 + return normalize(classPath.concat(Objects.requireNonNull(path))); + } + + /** + * 获取绝对路径,相对于ClassPath的目录
+ * 如果给定就是绝对路径,则返回原路径,原路径把所有\替换为/
+ * 兼容Spring风格的路径表示,例如:classpath:config/example.setting也会被识别后转换 + * + * @param path 相对路径 + * @return 绝对路径 + */ + public static String getAbsolutePath(String path) { + return getAbsolutePath(path, null); + } + + /** + * 获取标准的绝对路径 + * + * @param file 文件 + * @return 绝对路径 + */ + public static String getAbsolutePath(File file) { + if (file == null) { + return null; + } + + try { + return file.getCanonicalPath(); + } catch (IOException e) { + return file.getAbsolutePath(); + } + } + + /** + * 给定路径已经是绝对路径
+ * 此方法并没有针对路径做标准化,建议先执行{@link #normalize(String)}方法标准化路径后判断
+ * 绝对路径判断条件是: + *
    + *
  • 以/开头的路径
  • + *
  • 满足类似于 c:/xxxxx,其中祖母随意,不区分大小写
  • + *
  • 满足类似于 d:\xxxxx,其中祖母随意,不区分大小写
  • + *
+ * + * @param path 需要检查的Path + * @return 是否已经是绝对路径 + */ + public static boolean isAbsolutePath(String path) { + if (StrUtil.isEmpty(path)) { + return false; + } + + // 给定的路径已经是绝对路径了 + return StrUtil.C_SLASH == path.charAt(0) || ReUtil.isMatch(PATTERN_PATH_ABSOLUTE, path); + } + + /** + * 判断是否为目录,如果path为null,则返回false + * + * @param path 文件路径 + * @return 如果为目录true + */ + public static boolean isDirectory(String path) { + return (null != path) && file(path).isDirectory(); + } + + /** + * 判断是否为目录,如果file为null,则返回false + * + * @param file 文件 + * @return 如果为目录true + */ + public static boolean isDirectory(File file) { + return (null != file) && file.isDirectory(); + } + + /** + * 判断是否为文件,如果path为null,则返回false + * + * @param path 文件路径 + * @return 如果为文件true + */ + public static boolean isFile(String path) { + return (null != path) && file(path).isFile(); + } + + /** + * 判断是否为文件,如果file为null,则返回false + * + * @param file 文件 + * @return 如果为文件true + */ + public static boolean isFile(File file) { + return (null != file) && file.isFile(); + } + + /** + * 检查两个文件是否是同一个文件
+ * 所谓文件相同,是指File对象是否指向同一个文件或文件夹 + * + * @param file1 文件1 + * @param file2 文件2 + * @return 是否相同 + * @throws IORuntimeException IO异常 + */ + public static boolean equals(File file1, File file2) throws IORuntimeException { + Assert.notNull(file1); + Assert.notNull(file2); + if (!file1.exists() || !file2.exists()) { + // 两个文件都不存在判断其路径是否相同, 对于一个存在一个不存在的情况,一定不相同 + return !file1.exists()// + && !file2.exists()// + && pathEquals(file1, file2); + } + return equals(file1.toPath(), file2.toPath()); + } + + /** + * 比较两个文件内容是否相同
+ * 首先比较长度,长度一致再比较内容
+ * 此方法来自Apache Commons io + * + * @param file1 文件1 + * @param file2 文件2 + * @return 两个文件内容一致返回true,否则false + * @throws IORuntimeException IO异常 + * @since 4.0.6 + */ + public static boolean contentEquals(File file1, File file2) throws IORuntimeException { + boolean file1Exists = file1.exists(); + if (file1Exists != file2.exists()) { + return false; + } + + if (!file1Exists) { + // 两个文件都不存在,返回true + return true; + } + + if (file1.isDirectory() || file2.isDirectory()) { + // 不比较目录 + throw new IORuntimeException("Can't compare directories, only files"); + } + + if (file1.length() != file2.length()) { + // 文件长度不同 + return false; + } + + if (equals(file1, file2)) { + // 同一个文件 + return true; + } + + InputStream input1 = null; + InputStream input2 = null; + try { + input1 = getInputStream(file1); + input2 = getInputStream(file2); + return IoUtil.contentEquals(input1, input2); + + } finally { + IoUtil.close(input1); + IoUtil.close(input2); + } + } + + // ----------------------------------------------------------------------- + + /** + * 比较两个文件内容是否相同
+ * 首先比较长度,长度一致再比较内容,比较内容采用按行读取,每行比较
+ * 此方法来自Apache Commons io + * + * @param file1 文件1 + * @param file2 文件2 + * @param charset 编码,null表示使用平台默认编码 两个文件内容一致返回true,否则false + * @return 是否相同 + * @throws IORuntimeException IO异常 + * @since 4.0.6 + */ + public static boolean contentEqualsIgnoreEOL(File file1, File file2, Charset charset) throws IORuntimeException { + boolean file1Exists = file1.exists(); + if (file1Exists != file2.exists()) { + return false; + } + + if (!file1Exists) { + // 两个文件都不存在,返回true + return true; + } + + if (file1.isDirectory() || file2.isDirectory()) { + // 不比较目录 + throw new IORuntimeException("Can't compare directories, only files"); + } + + if (equals(file1, file2)) { + // 同一个文件 + return true; + } + + Reader input1 = null; + Reader input2 = null; + try { + input1 = getReader(file1, charset); + input2 = getReader(file2, charset); + return IoUtil.contentEqualsIgnoreEOL(input1, input2); + } finally { + IoUtil.close(input1); + IoUtil.close(input2); + } + } + + /** + * 文件路径是否相同
+ * 取两个文件的绝对路径比较,在Windows下忽略大小写,在Linux下不忽略。 + * + * @param file1 文件1 + * @param file2 文件2 + * @return 文件路径是否相同 + * @since 3.0.9 + */ + public static boolean pathEquals(File file1, File file2) { + if (isWindows()) { + // Windows环境 + try { + if (StrUtil.equalsIgnoreCase(file1.getCanonicalPath(), file2.getCanonicalPath())) { + return true; + } + } catch (Exception e) { + if (StrUtil.equalsIgnoreCase(file1.getAbsolutePath(), file2.getAbsolutePath())) { + return true; + } + } + } else { + // 类Unix环境 + try { + if (StrUtil.equals(file1.getCanonicalPath(), file2.getCanonicalPath())) { + return true; + } + } catch (Exception e) { + if (StrUtil.equals(file1.getAbsolutePath(), file2.getAbsolutePath())) { + return true; + } + } + } + return false; + } + + /** + * 获得最后一个文件路径分隔符的位置 + * + * @param filePath 文件路径 + * @return 最后一个文件路径分隔符的位置 + */ + public static int lastIndexOfSeparator(String filePath) { + if (StrUtil.isNotEmpty(filePath)) { + int i = filePath.length(); + char c; + while (--i >= 0) { + c = filePath.charAt(i); + if (CharUtil.isFileSeparator(c)) { + return i; + } + } + } + return -1; + } + + /** + * 判断文件是否被改动
+ * 如果文件对象为 null 或者文件不存在,被视为改动 + * + * @param file 文件对象 + * @param lastModifyTime 上次的改动时间 + * @return 是否被改动 + * @deprecated 拼写错误,请使用{@link #isModified(File, long)} + */ + @Deprecated + public static boolean isModifed(File file, long lastModifyTime) { + return isModified(file, lastModifyTime); + } + + + /** + * 判断文件是否被改动
+ * 如果文件对象为 null 或者文件不存在,被视为改动 + * + * @param file 文件对象 + * @param lastModifyTime 上次的改动时间 + * @return 是否被改动 + */ + public static boolean isModified(File file, long lastModifyTime) { + if (null == file || !file.exists()) { + return true; + } + return file.lastModified() != lastModifyTime; + } + + /** + * 修复路径
+ * 如果原路径尾部有分隔符,则保留为标准分隔符(/),否则不保留 + *
    + *
  1. 1. 统一用 /
  2. + *
  3. 2. 多个 / 转换为一个 /
  4. + *
  5. 3. 去除左边空格
  6. + *
  7. 4. .. 和 . 转换为绝对路径,当..多于已有路径时,直接返回根路径
  8. + *
+ *

+ * 栗子: + * + *

+	 * "/foo//" =》 "/foo/"
+	 * "/foo/./" =》 "/foo/"
+	 * "/foo/../bar" =》 "/bar"
+	 * "/foo/../bar/" =》 "/bar/"
+	 * "/foo/../bar/../baz" =》 "/baz"
+	 * "/../" =》 "/"
+	 * "foo/bar/.." =》 "foo"
+	 * "foo/../bar" =》 "bar"
+	 * "foo/../../bar" =》 "bar"
+	 * "//server/foo/../bar" =》 "/server/bar"
+	 * "//server/../bar" =》 "/bar"
+	 * "C:\\foo\\..\\bar" =》 "C:/bar"
+	 * "C:\\..\\bar" =》 "C:/bar"
+	 * "~/foo/../bar/" =》 "~/bar/"
+	 * "~/../bar" =》 普通用户运行是'bar的home目录',ROOT用户运行是'/bar'
+	 * 
+ * + * @param path 原路径 + * @return 修复后的路径 + */ + public static String normalize(String path) { + if (path == null) { + return null; + } + + // 兼容Spring风格的ClassPath路径,去除前缀,不区分大小写 + String pathToUse = StrUtil.removePrefixIgnoreCase(path, URLUtil.CLASSPATH_URL_PREFIX); + // 去除file:前缀 + pathToUse = StrUtil.removePrefixIgnoreCase(pathToUse, URLUtil.FILE_URL_PREFIX); + + // 识别home目录形式,并转换为绝对路径 + if (StrUtil.startWith(pathToUse, '~')) { + pathToUse = getUserHomePath() + pathToUse.substring(1); + } + + // 统一使用斜杠 + pathToUse = pathToUse.replaceAll("[/\\\\]+", StrUtil.SLASH); + // 去除开头空白符,末尾空白符合法,不去除 + pathToUse = StrUtil.trimStart(pathToUse); + //兼容Windows下的共享目录路径(原始路径如果以\\开头,则保留这种路径) + if (path.startsWith("\\\\")) { + pathToUse = "\\" + pathToUse; + } + + String prefix = StrUtil.EMPTY; + int prefixIndex = pathToUse.indexOf(StrUtil.COLON); + if (prefixIndex > -1) { + // 可能Windows风格路径 + prefix = pathToUse.substring(0, prefixIndex + 1); + if (StrUtil.startWith(prefix, StrUtil.C_SLASH)) { + // 去除类似于/C:这类路径开头的斜杠 + prefix = prefix.substring(1); + } + if (!prefix.contains(StrUtil.SLASH)) { + pathToUse = pathToUse.substring(prefixIndex + 1); + } else { + // 如果前缀中包含/,说明非Windows风格path + prefix = StrUtil.EMPTY; + } + } + if (pathToUse.startsWith(StrUtil.SLASH)) { + prefix += StrUtil.SLASH; + pathToUse = pathToUse.substring(1); + } + + List pathList = StrUtil.split(pathToUse, StrUtil.C_SLASH); + + List pathElements = new LinkedList<>(); + int tops = 0; + String element; + for (int i = pathList.size() - 1; i >= 0; i--) { + element = pathList.get(i); + // 只处理非.的目录,即只处理非当前目录 + if (!StrUtil.DOT.equals(element)) { + if (StrUtil.DOUBLE_DOT.equals(element)) { + tops++; + } else { + if (tops > 0) { + // 有上级目录标记时按照个数依次跳过 + tops--; + } else { + // Normal path element found. + pathElements.add(0, element); + } + } + } + } + + // issue#1703@Github + if (tops > 0 && StrUtil.isEmpty(prefix)) { + // 只有相对路径补充开头的..,绝对路径直接忽略之 + while (tops-- > 0) { + //遍历完节点发现还有上级标注(即开头有一个或多个..),补充之 + // Normal path element found. + pathElements.add(0, StrUtil.DOUBLE_DOT); + } + } + + return prefix + CollUtil.join(pathElements, StrUtil.SLASH); + } + + /** + * 获得相对子路径 + *

+ * 栗子: + * + *

+	 * dirPath: d:/aaa/bbb    filePath: d:/aaa/bbb/ccc     =》    ccc
+	 * dirPath: d:/Aaa/bbb    filePath: d:/aaa/bbb/ccc.txt     =》    ccc.txt
+	 * 
+ * + * @param rootDir 绝对父路径 + * @param file 文件 + * @return 相对子路径 + */ + public static String subPath(String rootDir, File file) { + try { + return subPath(rootDir, file.getCanonicalPath()); + } catch (IOException e) { + throw new IORuntimeException(e); + } + } + + /** + * 获得相对子路径,忽略大小写 + *

+ * 栗子: + * + *

+	 * dirPath: d:/aaa/bbb    filePath: d:/aaa/bbb/ccc     =》    ccc
+	 * dirPath: d:/Aaa/bbb    filePath: d:/aaa/bbb/ccc.txt     =》    ccc.txt
+	 * dirPath: d:/Aaa/bbb    filePath: d:/aaa/bbb/     =》    ""
+	 * 
+ * + * @param dirPath 父路径 + * @param filePath 文件路径 + * @return 相对子路径 + */ + public static String subPath(String dirPath, String filePath) { + if (StrUtil.isNotEmpty(dirPath) && StrUtil.isNotEmpty(filePath)) { + + dirPath = StrUtil.removeSuffix(normalize(dirPath), "/"); + filePath = normalize(filePath); + + final String result = StrUtil.removePrefixIgnoreCase(filePath, dirPath); + return StrUtil.removePrefix(result, "/"); + } + return filePath; + } + + // -------------------------------------------------------------------------------------------- name start + + /** + * 返回文件名 + * + * @param file 文件 + * @return 文件名 + * @see FileNameUtil#getName(File) + * @since 4.1.13 + */ + public static String getName(File file) { + return FileNameUtil.getName(file); + } + + /** + * 返回文件名
+ *
+	 * "d:/test/aaa" 返回 "aaa"
+	 * "/test/aaa.jpg" 返回 "aaa.jpg"
+	 * 
+ * + * @param filePath 文件 + * @return 文件名 + * @see FileNameUtil#getName(String) + * @since 4.1.13 + */ + public static String getName(String filePath) { + return FileNameUtil.getName(filePath); + } + + /** + * 获取文件后缀名,扩展名不带“.” + * + * @param file 文件 + * @return 扩展名 + * @see FileNameUtil#getSuffix(File) + * @since 5.3.8 + */ + public static String getSuffix(File file) { + return FileNameUtil.getSuffix(file); + } + + /** + * 获得文件后缀名,扩展名不带“.” + * + * @param fileName 文件名 + * @return 扩展名 + * @see FileNameUtil#getSuffix(String) + * @since 5.3.8 + */ + public static String getSuffix(String fileName) { + return FileNameUtil.getSuffix(fileName); + } + + /** + * 返回主文件名 + * + * @param file 文件 + * @return 主文件名 + * @see FileNameUtil#getPrefix(File) + * @since 5.3.8 + */ + public static String getPrefix(File file) { + return FileNameUtil.getPrefix(file); + } + + /** + * 返回主文件名 + * + * @param fileName 完整文件名 + * @return 主文件名 + * @see FileNameUtil#getPrefix(String) + * @since 5.3.8 + */ + public static String getPrefix(String fileName) { + return FileNameUtil.getPrefix(fileName); + } + + /** + * 返回主文件名 + * + * @param file 文件 + * @return 主文件名 + * @see FileNameUtil#mainName(File) + */ + public static String mainName(File file) { + return FileNameUtil.mainName(file); + } + + /** + * 返回主文件名 + * + * @param fileName 完整文件名 + * @return 主文件名 + * @see FileNameUtil#mainName(String) + */ + public static String mainName(String fileName) { + return FileNameUtil.mainName(fileName); + } + + /** + * 获取文件扩展名(后缀名),扩展名不带“.” + * + * @param file 文件 + * @return 扩展名 + * @see FileNameUtil#extName(File) + */ + public static String extName(File file) { + return FileNameUtil.extName(file); + } + + /** + * 获得文件的扩展名(后缀名),扩展名不带“.” + * + * @param fileName 文件名 + * @return 扩展名 + * @see FileNameUtil#extName(String) + */ + public static String extName(String fileName) { + return FileNameUtil.extName(fileName); + } + // -------------------------------------------------------------------------------------------- name end + + /** + * 判断文件路径是否有指定后缀,忽略大小写
+ * 常用语判断扩展名 + * + * @param file 文件或目录 + * @param suffix 后缀 + * @return 是否有指定后缀 + */ + public static boolean pathEndsWith(File file, String suffix) { + return file.getPath().toLowerCase().endsWith(suffix); + } + + /** + * 根据文件流的头部信息获得文件类型 + * + *
+	 *      1、无法识别类型默认按照扩展名识别
+	 *      2、xls、doc、msi头信息无法区分,按照扩展名区分
+	 *      3、zip可能为docx、xlsx、pptx、jar、war头信息无法区分,按照扩展名区分
+	 * 
+ * + * @param file 文件 {@link File} + * @return 类型,文件的扩展名,未找到为{@code null} + * @throws IORuntimeException IO异常 + * @see FileTypeUtil#getType(File) + */ + public static String getType(File file) throws IORuntimeException { + return FileTypeUtil.getType(file); + } + + // -------------------------------------------------------------------------------------------- in start + + /** + * 获得输入流 + * + * @param file 文件 + * @return 输入流 + * @throws IORuntimeException 文件未找到 + */ + public static BufferedInputStream getInputStream(File file) throws IORuntimeException { + return IoUtil.toBuffered(IoUtil.toStream(file)); + } + + /** + * 获得输入流 + * + * @param path 文件路径 + * @return 输入流 + * @throws IORuntimeException 文件未找到 + */ + public static BufferedInputStream getInputStream(String path) throws IORuntimeException { + return getInputStream(file(path)); + } + + /** + * 获得BOM输入流,用于处理带BOM头的文件 + * + * @param file 文件 + * @return 输入流 + * @throws IORuntimeException 文件未找到 + */ + public static BOMInputStream getBOMInputStream(File file) throws IORuntimeException { + try { + return new BOMInputStream(Files.newInputStream(file.toPath())); + } catch (IOException e) { + throw new IORuntimeException(e); + } + } + + /** + * 读取带BOM头的文件为Reader + * + * @param file 文件 + * @return BufferedReader对象 + * @since 5.5.8 + */ + public static BufferedReader getBOMReader(File file) { + return IoUtil.getReader(getBOMInputStream(file)); + } + + /** + * 获得一个文件读取器 + * + * @param file 文件 + * @return BufferedReader对象 + * @throws IORuntimeException IO异常 + */ + public static BufferedReader getUtf8Reader(File file) throws IORuntimeException { + return getReader(file, CharsetUtil.CHARSET_UTF_8); + } + + /** + * 获得一个文件读取器 + * + * @param path 文件路径 + * @return BufferedReader对象 + * @throws IORuntimeException IO异常 + */ + public static BufferedReader getUtf8Reader(String path) throws IORuntimeException { + return getReader(path, CharsetUtil.CHARSET_UTF_8); + } + + /** + * 获得一个文件读取器 + * + * @param file 文件 + * @param charsetName 字符集 + * @return BufferedReader对象 + * @throws IORuntimeException IO异常 + * @deprecated 请使用 {@link #getReader(File, Charset)} + */ + @Deprecated + public static BufferedReader getReader(File file, String charsetName) throws IORuntimeException { + return IoUtil.getReader(getInputStream(file), CharsetUtil.charset(charsetName)); + } + + /** + * 获得一个文件读取器 + * + * @param file 文件 + * @param charset 字符集 + * @return BufferedReader对象 + * @throws IORuntimeException IO异常 + */ + public static BufferedReader getReader(File file, Charset charset) throws IORuntimeException { + return IoUtil.getReader(getInputStream(file), charset); + } + + /** + * 获得一个文件读取器 + * + * @param path 绝对路径 + * @param charsetName 字符集 + * @return BufferedReader对象 + * @throws IORuntimeException IO异常 + * @deprecated 请使用 {@link #getReader(String, Charset)} + */ + @Deprecated + public static BufferedReader getReader(String path, String charsetName) throws IORuntimeException { + return getReader(path, CharsetUtil.charset(charsetName)); + } + + /** + * 获得一个文件读取器 + * + * @param path 绝对路径 + * @param charset 字符集 + * @return BufferedReader对象 + * @throws IORuntimeException IO异常 + */ + public static BufferedReader getReader(String path, Charset charset) throws IORuntimeException { + return getReader(file(path), charset); + } + + // -------------------------------------------------------------------------------------------- in end + + /** + * 读取文件所有数据
+ * 文件的长度不能超过Integer.MAX_VALUE + * + * @param file 文件 + * @return 字节码 + * @throws IORuntimeException IO异常 + */ + public static byte[] readBytes(File file) throws IORuntimeException { + return FileReader.create(file).readBytes(); + } + + /** + * 读取文件所有数据
+ * 文件的长度不能超过Integer.MAX_VALUE + * + * @param filePath 文件路径 + * @return 字节码 + * @throws IORuntimeException IO异常 + * @since 3.2.0 + */ + public static byte[] readBytes(String filePath) throws IORuntimeException { + return readBytes(file(filePath)); + } + + /** + * 读取文件内容 + * + * @param file 文件 + * @return 内容 + * @throws IORuntimeException IO异常 + */ + public static String readUtf8String(File file) throws IORuntimeException { + return readString(file, CharsetUtil.CHARSET_UTF_8); + } + + /** + * 读取文件内容 + * + * @param path 文件路径 + * @return 内容 + * @throws IORuntimeException IO异常 + */ + public static String readUtf8String(String path) throws IORuntimeException { + return readString(path, CharsetUtil.CHARSET_UTF_8); + } + + /** + * 读取文件内容 + * + * @param file 文件 + * @param charsetName 字符集 + * @return 内容 + * @throws IORuntimeException IO异常 + * @deprecated 请使用 {@link #readString(File, Charset)} + */ + @Deprecated + public static String readString(File file, String charsetName) throws IORuntimeException { + return readString(file, CharsetUtil.charset(charsetName)); + } + + /** + * 读取文件内容 + * + * @param file 文件 + * @param charset 字符集 + * @return 内容 + * @throws IORuntimeException IO异常 + */ + public static String readString(File file, Charset charset) throws IORuntimeException { + return FileReader.create(file, charset).readString(); + } + + /** + * 读取文件内容 + * + * @param path 文件路径 + * @param charsetName 字符集 + * @return 内容 + * @throws IORuntimeException IO异常 + * @deprecated 请使用 {@link #readString(String, Charset)} + */ + @Deprecated + public static String readString(String path, String charsetName) throws IORuntimeException { + return readString(path, CharsetUtil.charset(charsetName)); + } + + /** + * 读取文件内容 + * + * @param path 文件路径 + * @param charset 字符集 + * @return 内容 + * @throws IORuntimeException IO异常 + */ + public static String readString(String path, Charset charset) throws IORuntimeException { + return readString(file(path), charset); + } + + /** + * 读取文件内容 + * + * @param url 文件URL + * @param charsetName 字符集 + * @return 内容 + * @throws IORuntimeException IO异常 + * @deprecated 请使用 {@link #readString(URL, Charset)} + */ + @Deprecated + public static String readString(URL url, String charsetName) throws IORuntimeException { + return readString(url, CharsetUtil.charset(charsetName)); + } + + /** + * 读取文件内容 + * + * @param url 文件URL + * @param charset 字符集 + * @return 内容 + * @throws IORuntimeException IO异常 + * @since 5.7.10 + */ + public static String readString(URL url, Charset charset) throws IORuntimeException { + if (url == null) { + throw new NullPointerException("Empty url provided!"); + } + + InputStream in = null; + try { + in = url.openStream(); + return IoUtil.read(in, charset); + } catch (IOException e) { + throw new IORuntimeException(e); + } finally { + IoUtil.close(in); + } + } + + /** + * 从文件中读取每一行的UTF-8编码数据 + * + * @param 集合类型 + * @param path 文件路径 + * @param collection 集合 + * @return 文件中的每行内容的集合 + * @throws IORuntimeException IO异常 + * @since 3.1.1 + */ + public static > T readUtf8Lines(String path, T collection) throws IORuntimeException { + return readLines(path, CharsetUtil.CHARSET_UTF_8, collection); + } + + /** + * 从文件中读取每一行数据 + * + * @param 集合类型 + * @param path 文件路径 + * @param charset 字符集 + * @param collection 集合 + * @return 文件中的每行内容的集合 + * @throws IORuntimeException IO异常 + */ + public static > T readLines(String path, String charset, T collection) throws IORuntimeException { + return readLines(file(path), charset, collection); + } + + /** + * 从文件中读取每一行数据 + * + * @param 集合类型 + * @param path 文件路径 + * @param charset 字符集 + * @param collection 集合 + * @return 文件中的每行内容的集合 + * @throws IORuntimeException IO异常 + */ + public static > T readLines(String path, Charset charset, T collection) throws IORuntimeException { + return readLines(file(path), charset, collection); + } + + /** + * 从文件中读取每一行数据,数据编码为UTF-8 + * + * @param 集合类型 + * @param file 文件路径 + * @param collection 集合 + * @return 文件中的每行内容的集合 + * @throws IORuntimeException IO异常 + * @since 3.1.1 + */ + public static > T readUtf8Lines(File file, T collection) throws IORuntimeException { + return readLines(file, CharsetUtil.CHARSET_UTF_8, collection); + } + + /** + * 从文件中读取每一行数据 + * + * @param 集合类型 + * @param file 文件路径 + * @param charset 字符集 + * @param collection 集合 + * @return 文件中的每行内容的集合 + * @throws IORuntimeException IO异常 + */ + public static > T readLines(File file, String charset, T collection) throws IORuntimeException { + return FileReader.create(file, CharsetUtil.charset(charset)).readLines(collection); + } + + /** + * 从文件中读取每一行数据 + * + * @param 集合类型 + * @param file 文件路径 + * @param charset 字符集 + * @param collection 集合 + * @return 文件中的每行内容的集合 + * @throws IORuntimeException IO异常 + */ + public static > T readLines(File file, Charset charset, T collection) throws IORuntimeException { + return FileReader.create(file, charset).readLines(collection); + } + + /** + * 从文件中读取每一行数据,编码为UTF-8 + * + * @param 集合类型 + * @param url 文件的URL + * @param collection 集合 + * @return 文件中的每行内容的集合 + * @throws IORuntimeException IO异常 + */ + public static > T readUtf8Lines(URL url, T collection) throws IORuntimeException { + return readLines(url, CharsetUtil.CHARSET_UTF_8, collection); + } + + /** + * 从文件中读取每一行数据 + * + * @param 集合类型 + * @param url 文件的URL + * @param charsetName 字符集 + * @param collection 集合 + * @return 文件中的每行内容的集合 + * @throws IORuntimeException IO异常 + * @deprecated 请使用 {@link #readLines(URL, Charset, Collection)} + */ + @Deprecated + public static > T readLines(URL url, String charsetName, T collection) throws IORuntimeException { + return readLines(url, CharsetUtil.charset(charsetName), collection); + } + + /** + * 从文件中读取每一行数据 + * + * @param 集合类型 + * @param url 文件的URL + * @param charset 字符集 + * @param collection 集合 + * @return 文件中的每行内容的集合 + * @throws IORuntimeException IO异常 + * @since 3.1.1 + */ + public static > T readLines(URL url, Charset charset, T collection) throws IORuntimeException { + InputStream in = null; + try { + in = url.openStream(); + return IoUtil.readLines(in, charset, collection); + } catch (IOException e) { + throw new IORuntimeException(e); + } finally { + IoUtil.close(in); + } + } + + /** + * 从文件中读取每一行数据 + * + * @param url 文件的URL + * @return 文件中的每行内容的集合List + * @throws IORuntimeException IO异常 + */ + public static List readUtf8Lines(URL url) throws IORuntimeException { + return readLines(url, CharsetUtil.CHARSET_UTF_8); + } + + /** + * 从文件中读取每一行数据 + * + * @param url 文件的URL + * @param charsetName 字符集 + * @return 文件中的每行内容的集合List + * @throws IORuntimeException IO异常 + * @deprecated 请使用 {@link #readLines(URL, Charset)} + */ + @Deprecated + public static List readLines(URL url, String charsetName) throws IORuntimeException { + return readLines(url, CharsetUtil.charset(charsetName)); + } + + /** + * 从文件中读取每一行数据 + * + * @param url 文件的URL + * @param charset 字符集 + * @return 文件中的每行内容的集合List + * @throws IORuntimeException IO异常 + */ + public static List readLines(URL url, Charset charset) throws IORuntimeException { + return readLines(url, charset, new ArrayList<>()); + } + + /** + * 从文件中读取每一行数据,编码为UTF-8 + * + * @param path 文件路径 + * @return 文件中的每行内容的集合List + * @throws IORuntimeException IO异常 + * @since 3.1.1 + */ + public static List readUtf8Lines(String path) throws IORuntimeException { + return readLines(path, CharsetUtil.CHARSET_UTF_8); + } + + /** + * 从文件中读取每一行数据 + * + * @param path 文件路径 + * @param charset 字符集 + * @return 文件中的每行内容的集合List + * @throws IORuntimeException IO异常 + */ + public static List readLines(String path, String charset) throws IORuntimeException { + return readLines(path, charset, new ArrayList<>()); + } + + /** + * 从文件中读取每一行数据 + * + * @param path 文件路径 + * @param charset 字符集 + * @return 文件中的每行内容的集合List + * @throws IORuntimeException IO异常 + * @since 3.1.1 + */ + public static List readLines(String path, Charset charset) throws IORuntimeException { + return readLines(path, charset, new ArrayList<>()); + } + + /** + * 从文件中读取每一行数据 + * + * @param file 文件 + * @return 文件中的每行内容的集合List + * @throws IORuntimeException IO异常 + * @since 3.1.1 + */ + public static List readUtf8Lines(File file) throws IORuntimeException { + return readLines(file, CharsetUtil.CHARSET_UTF_8); + } + + /** + * 从文件中读取每一行数据 + * + * @param file 文件 + * @param charset 字符集 + * @return 文件中的每行内容的集合List + * @throws IORuntimeException IO异常 + */ + public static List readLines(File file, String charset) throws IORuntimeException { + return readLines(file, charset, new ArrayList<>()); + } + + /** + * 从文件中读取每一行数据 + * + * @param file 文件 + * @param charset 字符集 + * @return 文件中的每行内容的集合List + * @throws IORuntimeException IO异常 + */ + public static List readLines(File file, Charset charset) throws IORuntimeException { + return readLines(file, charset, new ArrayList<>()); + } + + /** + * 按行处理文件内容,编码为UTF-8 + * + * @param file 文件 + * @param lineHandler {@link LineHandler}行处理器 + * @throws IORuntimeException IO异常 + */ + public static void readUtf8Lines(File file, LineHandler lineHandler) throws IORuntimeException { + readLines(file, CharsetUtil.CHARSET_UTF_8, lineHandler); + } + + /** + * 按行处理文件内容 + * + * @param file 文件 + * @param charset 编码 + * @param lineHandler {@link LineHandler}行处理器 + * @throws IORuntimeException IO异常 + */ + public static void readLines(File file, Charset charset, LineHandler lineHandler) throws IORuntimeException { + FileReader.create(file, charset).readLines(lineHandler); + } + + /** + * 按行处理文件内容 + * + * @param file {@link RandomAccessFile}文件 + * @param charset 编码 + * @param lineHandler {@link LineHandler}行处理器 + * @throws IORuntimeException IO异常 + * @since 4.5.2 + */ + public static void readLines(RandomAccessFile file, Charset charset, LineHandler lineHandler) { + String line; + try { + while ((line = file.readLine()) != null) { + lineHandler.handle(CharsetUtil.convert(line, CharsetUtil.CHARSET_ISO_8859_1, charset)); + } + } catch (IOException e) { + throw new IORuntimeException(e); + } + } + + /** + * 单行处理文件内容 + * + * @param file {@link RandomAccessFile}文件 + * @param charset 编码 + * @param lineHandler {@link LineHandler}行处理器 + * @throws IORuntimeException IO异常 + * @since 4.5.2 + */ + public static void readLine(RandomAccessFile file, Charset charset, LineHandler lineHandler) { + final String line = readLine(file, charset); + if (null != line) { + lineHandler.handle(line); + } + } + + /** + * 单行处理文件内容 + * + * @param file {@link RandomAccessFile}文件 + * @param charset 编码 + * @return 行内容 + * @throws IORuntimeException IO异常 + * @since 4.5.18 + */ + public static String readLine(RandomAccessFile file, Charset charset) { + String line; + try { + line = file.readLine(); + } catch (IOException e) { + throw new IORuntimeException(e); + } + if (null != line) { + return CharsetUtil.convert(line, CharsetUtil.CHARSET_ISO_8859_1, charset); + } + + return null; + } + + /** + * 按照给定的readerHandler读取文件中的数据 + * + * @param 集合类型 + * @param readerHandler Reader处理类 + * @param path 文件的绝对路径 + * @return 从文件中load出的数据 + * @throws IORuntimeException IO异常 + * @since 3.1.1 + */ + public static T loadUtf8(String path, ReaderHandler readerHandler) throws IORuntimeException { + return load(path, CharsetUtil.CHARSET_UTF_8, readerHandler); + } + + /** + * 按照给定的readerHandler读取文件中的数据 + * + * @param 集合类型 + * @param readerHandler Reader处理类 + * @param path 文件的绝对路径 + * @param charset 字符集 + * @return 从文件中load出的数据 + * @throws IORuntimeException IO异常 + * @since 3.1.1 + */ + public static T load(String path, String charset, ReaderHandler readerHandler) throws IORuntimeException { + return FileReader.create(file(path), CharsetUtil.charset(charset)).read(readerHandler); + } + + /** + * 按照给定的readerHandler读取文件中的数据 + * + * @param 集合类型 + * @param readerHandler Reader处理类 + * @param path 文件的绝对路径 + * @param charset 字符集 + * @return 从文件中load出的数据 + * @throws IORuntimeException IO异常 + * @since 3.1.1 + */ + public static T load(String path, Charset charset, ReaderHandler readerHandler) throws IORuntimeException { + return FileReader.create(file(path), charset).read(readerHandler); + } + + /** + * 按照给定的readerHandler读取文件中的数据 + * + * @param 集合类型 + * @param readerHandler Reader处理类 + * @param file 文件 + * @return 从文件中load出的数据 + * @throws IORuntimeException IO异常 + * @since 3.1.1 + */ + public static T loadUtf8(File file, ReaderHandler readerHandler) throws IORuntimeException { + return load(file, CharsetUtil.CHARSET_UTF_8, readerHandler); + } + + /** + * 按照给定的readerHandler读取文件中的数据 + * + * @param 集合类型 + * @param readerHandler Reader处理类 + * @param file 文件 + * @param charset 字符集 + * @return 从文件中load出的数据 + * @throws IORuntimeException IO异常 + * @since 3.1.1 + */ + public static T load(File file, Charset charset, ReaderHandler readerHandler) throws IORuntimeException { + return FileReader.create(file, charset).read(readerHandler); + } + + // -------------------------------------------------------------------------------------------- out start + + /** + * 获得一个输出流对象 + * + * @param file 文件 + * @return 输出流对象 + * @throws IORuntimeException IO异常 + */ + public static BufferedOutputStream getOutputStream(File file) throws IORuntimeException { + final OutputStream out; + try { + out = Files.newOutputStream(touch(file).toPath()); + } catch (final IOException e) { + throw new IORuntimeException(e); + } + return IoUtil.toBuffered(out); + } + + /** + * 获得一个输出流对象 + * + * @param path 输出到的文件路径,绝对路径 + * @return 输出流对象 + * @throws IORuntimeException IO异常 + */ + public static BufferedOutputStream getOutputStream(String path) throws IORuntimeException { + return getOutputStream(touch(path)); + } + + /** + * 获得一个带缓存的写入对象 + * + * @param path 输出路径,绝对路径 + * @param charsetName 字符集 + * @param isAppend 是否追加 + * @return BufferedReader对象 + * @throws IORuntimeException IO异常 + * @deprecated 请使用 {@link #getWriter(String, Charset, boolean)} + */ + @Deprecated + public static BufferedWriter getWriter(String path, String charsetName, boolean isAppend) throws IORuntimeException { + return getWriter(path, Charset.forName(charsetName), isAppend); + } + + /** + * 获得一个带缓存的写入对象 + * + * @param path 输出路径,绝对路径 + * @param charset 字符集 + * @param isAppend 是否追加 + * @return BufferedReader对象 + * @throws IORuntimeException IO异常 + */ + public static BufferedWriter getWriter(String path, Charset charset, boolean isAppend) throws IORuntimeException { + return getWriter(touch(path), charset, isAppend); + } + + /** + * 获得一个带缓存的写入对象 + * + * @param file 输出文件 + * @param charsetName 字符集 + * @param isAppend 是否追加 + * @return BufferedReader对象 + * @throws IORuntimeException IO异常 + * @deprecated 请使用 {@link #getWriter(File, Charset, boolean)} + */ + @Deprecated + public static BufferedWriter getWriter(File file, String charsetName, boolean isAppend) throws IORuntimeException { + return getWriter(file, Charset.forName(charsetName), isAppend); + } + + /** + * 获得一个带缓存的写入对象 + * + * @param file 输出文件 + * @param charset 字符集 + * @param isAppend 是否追加 + * @return BufferedReader对象 + * @throws IORuntimeException IO异常 + */ + public static BufferedWriter getWriter(File file, Charset charset, boolean isAppend) throws IORuntimeException { + return FileWriter.create(file, charset).getWriter(isAppend); + } + + /** + * 获得一个打印写入对象,可以有print + * + * @param path 输出路径,绝对路径 + * @param charset 字符集 + * @param isAppend 是否追加 + * @return 打印对象 + * @throws IORuntimeException IO异常 + */ + public static PrintWriter getPrintWriter(String path, String charset, boolean isAppend) throws IORuntimeException { + return new PrintWriter(getWriter(path, charset, isAppend)); + } + + /** + * 获得一个打印写入对象,可以有print + * + * @param path 输出路径,绝对路径 + * @param charset 字符集 + * @param isAppend 是否追加 + * @return 打印对象 + * @throws IORuntimeException IO异常 + * @since 4.1.1 + */ + public static PrintWriter getPrintWriter(String path, Charset charset, boolean isAppend) throws IORuntimeException { + return new PrintWriter(getWriter(path, charset, isAppend)); + } + + /** + * 获得一个打印写入对象,可以有print + * + * @param file 文件 + * @param charset 字符集 + * @param isAppend 是否追加 + * @return 打印对象 + * @throws IORuntimeException IO异常 + */ + public static PrintWriter getPrintWriter(File file, String charset, boolean isAppend) throws IORuntimeException { + return new PrintWriter(getWriter(file, charset, isAppend)); + } + + /** + * 获得一个打印写入对象,可以有print + * + * @param file 文件 + * @param charset 字符集 + * @param isAppend 是否追加 + * @return 打印对象 + * @throws IORuntimeException IO异常 + * @since 5.4.3 + */ + public static PrintWriter getPrintWriter(File file, Charset charset, boolean isAppend) throws IORuntimeException { + return new PrintWriter(getWriter(file, charset, isAppend)); + } + + /** + * 获取当前系统的换行分隔符 + * + *
+	 * Windows: \r\n
+	 * Mac: \r
+	 * Linux: \n
+	 * 
+ * + * @return 换行符 + * @since 4.0.5 + */ + public static String getLineSeparator() { + return System.lineSeparator(); + // return System.getProperty("line.separator"); + } + + // -------------------------------------------------------------------------------------------- out end + + /** + * 将String写入文件,覆盖模式,字符集为UTF-8 + * + * @param content 写入的内容 + * @param path 文件路径 + * @return 写入的文件 + * @throws IORuntimeException IO异常 + */ + public static File writeUtf8String(String content, String path) throws IORuntimeException { + return writeString(content, path, CharsetUtil.CHARSET_UTF_8); + } + + /** + * 将String写入文件,覆盖模式,字符集为UTF-8 + * + * @param content 写入的内容 + * @param file 文件 + * @return 写入的文件 + * @throws IORuntimeException IO异常 + */ + public static File writeUtf8String(String content, File file) throws IORuntimeException { + return writeString(content, file, CharsetUtil.CHARSET_UTF_8); + } + + /** + * 将String写入文件,覆盖模式 + * + * @param content 写入的内容 + * @param path 文件路径 + * @param charset 字符集 + * @return 写入的文件 + * @throws IORuntimeException IO异常 + */ + public static File writeString(String content, String path, String charset) throws IORuntimeException { + return writeString(content, touch(path), charset); + } + + /** + * 将String写入文件,覆盖模式 + * + * @param content 写入的内容 + * @param path 文件路径 + * @param charset 字符集 + * @return 写入的文件 + * @throws IORuntimeException IO异常 + */ + public static File writeString(String content, String path, Charset charset) throws IORuntimeException { + return writeString(content, touch(path), charset); + } + + /** + * 将String写入文件,覆盖模式 + * + * @param content 写入的内容 + * @param file 文件 + * @param charset 字符集 + * @return 被写入的文件 + * @throws IORuntimeException IO异常 + */ + public static File writeString(String content, File file, String charset) throws IORuntimeException { + return FileWriter.create(file, CharsetUtil.charset(charset)).write(content); + } + + /** + * 将String写入文件,覆盖模式 + * + * @param content 写入的内容 + * @param file 文件 + * @param charset 字符集 + * @return 被写入的文件 + * @throws IORuntimeException IO异常 + */ + public static File writeString(String content, File file, Charset charset) throws IORuntimeException { + return FileWriter.create(file, charset).write(content); + } + + /** + * 将String写入文件,UTF-8编码追加模式 + * + * @param content 写入的内容 + * @param path 文件路径 + * @return 写入的文件 + * @throws IORuntimeException IO异常 + * @since 3.1.2 + */ + public static File appendUtf8String(String content, String path) throws IORuntimeException { + return appendString(content, path, CharsetUtil.CHARSET_UTF_8); + } + + /** + * 将String写入文件,追加模式 + * + * @param content 写入的内容 + * @param path 文件路径 + * @param charset 字符集 + * @return 写入的文件 + * @throws IORuntimeException IO异常 + */ + public static File appendString(String content, String path, String charset) throws IORuntimeException { + return appendString(content, touch(path), charset); + } + + /** + * 将String写入文件,追加模式 + * + * @param content 写入的内容 + * @param path 文件路径 + * @param charset 字符集 + * @return 写入的文件 + * @throws IORuntimeException IO异常 + */ + public static File appendString(String content, String path, Charset charset) throws IORuntimeException { + return appendString(content, touch(path), charset); + } + + /** + * 将String写入文件,UTF-8编码追加模式 + * + * @param content 写入的内容 + * @param file 文件 + * @return 写入的文件 + * @throws IORuntimeException IO异常 + * @since 3.1.2 + */ + public static File appendUtf8String(String content, File file) throws IORuntimeException { + return appendString(content, file, CharsetUtil.CHARSET_UTF_8); + } + + /** + * 将String写入文件,追加模式 + * + * @param content 写入的内容 + * @param file 文件 + * @param charset 字符集 + * @return 写入的文件 + * @throws IORuntimeException IO异常 + */ + public static File appendString(String content, File file, String charset) throws IORuntimeException { + return FileWriter.create(file, CharsetUtil.charset(charset)).append(content); + } + + /** + * 将String写入文件,追加模式 + * + * @param content 写入的内容 + * @param file 文件 + * @param charset 字符集 + * @return 写入的文件 + * @throws IORuntimeException IO异常 + */ + public static File appendString(String content, File file, Charset charset) throws IORuntimeException { + return FileWriter.create(file, charset).append(content); + } + + /** + * 将列表写入文件,覆盖模式,编码为UTF-8 + * + * @param 集合元素类型 + * @param list 列表 + * @param path 绝对路径 + * @return 目标文件 + * @throws IORuntimeException IO异常 + * @since 3.2.0 + */ + public static File writeUtf8Lines(Collection list, String path) throws IORuntimeException { + return writeLines(list, path, CharsetUtil.CHARSET_UTF_8); + } + + /** + * 将列表写入文件,覆盖模式,编码为UTF-8 + * + * @param 集合元素类型 + * @param list 列表 + * @param file 绝对路径 + * @return 目标文件 + * @throws IORuntimeException IO异常 + * @since 3.2.0 + */ + public static File writeUtf8Lines(Collection list, File file) throws IORuntimeException { + return writeLines(list, file, CharsetUtil.CHARSET_UTF_8); + } + + /** + * 将列表写入文件,覆盖模式 + * + * @param 集合元素类型 + * @param list 列表 + * @param path 绝对路径 + * @param charset 字符集 + * @return 目标文件 + * @throws IORuntimeException IO异常 + */ + public static File writeLines(Collection list, String path, String charset) throws IORuntimeException { + return writeLines(list, path, charset, false); + } + + /** + * 将列表写入文件,覆盖模式 + * + * @param 集合元素类型 + * @param list 列表 + * @param path 绝对路径 + * @param charset 字符集 + * @return 目标文件 + * @throws IORuntimeException IO异常 + */ + public static File writeLines(Collection list, String path, Charset charset) throws IORuntimeException { + return writeLines(list, path, charset, false); + } + + /** + * 将列表写入文件,覆盖模式 + * + * @param 集合元素类型 + * @param list 列表 + * @param file 文件 + * @param charset 字符集 + * @return 目标文件 + * @throws IORuntimeException IO异常 + * @since 4.2.0 + */ + public static File writeLines(Collection list, File file, String charset) throws IORuntimeException { + return writeLines(list, file, charset, false); + } + + /** + * 将列表写入文件,覆盖模式 + * + * @param 集合元素类型 + * @param list 列表 + * @param file 文件 + * @param charset 字符集 + * @return 目标文件 + * @throws IORuntimeException IO异常 + * @since 4.2.0 + */ + public static File writeLines(Collection list, File file, Charset charset) throws IORuntimeException { + return writeLines(list, file, charset, false); + } + + /** + * 将列表写入文件,追加模式 + * + * @param 集合元素类型 + * @param list 列表 + * @param file 文件 + * @return 目标文件 + * @throws IORuntimeException IO异常 + * @since 3.1.2 + */ + public static File appendUtf8Lines(Collection list, File file) throws IORuntimeException { + return appendLines(list, file, CharsetUtil.CHARSET_UTF_8); + } + + /** + * 将列表写入文件,追加模式 + * + * @param 集合元素类型 + * @param list 列表 + * @param path 文件路径 + * @return 目标文件 + * @throws IORuntimeException IO异常 + * @since 3.1.2 + */ + public static File appendUtf8Lines(Collection list, String path) throws IORuntimeException { + return appendLines(list, path, CharsetUtil.CHARSET_UTF_8); + } + + /** + * 将列表写入文件,追加模式 + * + * @param 集合元素类型 + * @param list 列表 + * @param path 绝对路径 + * @param charset 字符集 + * @return 目标文件 + * @throws IORuntimeException IO异常 + */ + public static File appendLines(Collection list, String path, String charset) throws IORuntimeException { + return writeLines(list, path, charset, true); + } + + /** + * 将列表写入文件,追加模式 + * + * @param 集合元素类型 + * @param list 列表 + * @param file 文件 + * @param charset 字符集 + * @return 目标文件 + * @throws IORuntimeException IO异常 + * @since 3.1.2 + */ + public static File appendLines(Collection list, File file, String charset) throws IORuntimeException { + return writeLines(list, file, charset, true); + } + + /** + * 将列表写入文件,追加模式 + * + * @param 集合元素类型 + * @param list 列表 + * @param path 绝对路径 + * @param charset 字符集 + * @return 目标文件 + * @throws IORuntimeException IO异常 + */ + public static File appendLines(Collection list, String path, Charset charset) throws IORuntimeException { + return writeLines(list, path, charset, true); + } + + /** + * 将列表写入文件,追加模式,策略为: + *
    + *
  • 当文件为空,从开头追加,尾部不加空行
  • + *
  • 当有内容,换行追加,尾部不加空行
  • + *
  • 当有内容,并末尾有空行,依旧换行追加
  • + *
+ * + * @param 集合元素类型 + * @param list 列表 + * @param file 文件 + * @param charset 字符集 + * @return 目标文件 + * @throws IORuntimeException IO异常 + * @since 3.1.2 + */ + public static File appendLines(Collection list, File file, Charset charset) throws IORuntimeException { + return writeLines(list, file, charset, true); + } + + /** + * 将列表写入文件 + * + * @param 集合元素类型 + * @param list 列表 + * @param path 文件路径 + * @param charset 字符集 + * @param isAppend 是否追加 + * @return 目标文件 + * @throws IORuntimeException IO异常 + */ + public static File writeLines(Collection list, String path, String charset, boolean isAppend) throws IORuntimeException { + return writeLines(list, file(path), charset, isAppend); + } + + /** + * 将列表写入文件 + * + * @param 集合元素类型 + * @param list 列表 + * @param path 文件路径 + * @param charset 字符集 + * @param isAppend 是否追加 + * @return 目标文件 + * @throws IORuntimeException IO异常 + */ + public static File writeLines(Collection list, String path, Charset charset, boolean isAppend) throws IORuntimeException { + return writeLines(list, file(path), charset, isAppend); + } + + /** + * 将列表写入文件 + * + * @param 集合元素类型 + * @param list 列表 + * @param file 文件 + * @param charset 字符集 + * @param isAppend 是否追加 + * @return 目标文件 + * @throws IORuntimeException IO异常 + */ + public static File writeLines(Collection list, File file, String charset, boolean isAppend) throws IORuntimeException { + return FileWriter.create(file, CharsetUtil.charset(charset)).writeLines(list, isAppend); + } + + /** + * 将列表写入文件 + * + * @param 集合元素类型 + * @param list 列表 + * @param file 文件 + * @param charset 字符集 + * @param isAppend 是否追加 + * @return 目标文件 + * @throws IORuntimeException IO异常 + */ + public static File writeLines(Collection list, File file, Charset charset, boolean isAppend) throws IORuntimeException { + return FileWriter.create(file, charset).writeLines(list, isAppend); + } + + /** + * 将Map写入文件,每个键值对为一行,一行中键与值之间使用kvSeparator分隔 + * + * @param map Map + * @param file 文件 + * @param kvSeparator 键和值之间的分隔符,如果传入null使用默认分隔符" = " + * @param isAppend 是否追加 + * @return 目标文件 + * @throws IORuntimeException IO异常 + * @since 4.0.5 + */ + public static File writeUtf8Map(Map map, File file, String kvSeparator, boolean isAppend) throws IORuntimeException { + return FileWriter.create(file, CharsetUtil.CHARSET_UTF_8).writeMap(map, kvSeparator, isAppend); + } + + /** + * 将Map写入文件,每个键值对为一行,一行中键与值之间使用kvSeparator分隔 + * + * @param map Map + * @param file 文件 + * @param charset 字符集编码 + * @param kvSeparator 键和值之间的分隔符,如果传入null使用默认分隔符" = " + * @param isAppend 是否追加 + * @return 目标文件 + * @throws IORuntimeException IO异常 + * @since 4.0.5 + */ + public static File writeMap(Map map, File file, Charset charset, String kvSeparator, boolean isAppend) throws IORuntimeException { + return FileWriter.create(file, charset).writeMap(map, kvSeparator, isAppend); + } + + /** + * 写数据到文件中
+ * 文件路径如果是相对路径,则相对ClassPath + * + * @param data 数据 + * @param path 相对ClassPath的目录或者绝对路径目录 + * @return 目标文件 + * @throws IORuntimeException IO异常 + */ + public static File writeBytes(byte[] data, String path) throws IORuntimeException { + return writeBytes(data, touch(path)); + } + + /** + * 写数据到文件中 + * + * @param dest 目标文件 + * @param data 数据 + * @return 目标文件 + * @throws IORuntimeException IO异常 + */ + public static File writeBytes(byte[] data, File dest) throws IORuntimeException { + return writeBytes(data, dest, 0, data.length, false); + } + + /** + * 写入数据到文件 + * + * @param data 数据 + * @param dest 目标文件 + * @param off 数据开始位置 + * @param len 数据长度 + * @param isAppend 是否追加模式 + * @return 目标文件 + * @throws IORuntimeException IO异常 + */ + public static File writeBytes(byte[] data, File dest, int off, int len, boolean isAppend) throws IORuntimeException { + return FileWriter.create(dest).write(data, off, len, isAppend); + } + + /** + * 将流的内容写入文件
+ * 此方法会自动关闭输入流 + * + * @param dest 目标文件 + * @param in 输入流 + * @return dest + * @throws IORuntimeException IO异常 + */ + public static File writeFromStream(InputStream in, File dest) throws IORuntimeException { + return writeFromStream(in, dest, true); + } + + /** + * 将流的内容写入文件 + * + * @param dest 目标文件 + * @param in 输入流 + * @param isCloseIn 是否关闭输入流 + * @return dest + * @throws IORuntimeException IO异常 + * @since 5.5.6 + */ + public static File writeFromStream(InputStream in, File dest, boolean isCloseIn) throws IORuntimeException { + return FileWriter.create(dest).writeFromStream(in, isCloseIn); + } + + /** + * 将流的内容写入文件
+ * 此方法会自动关闭输入流 + * + * @param in 输入流 + * @param fullFilePath 文件绝对路径 + * @return 目标文件 + * @throws IORuntimeException IO异常 + */ + public static File writeFromStream(InputStream in, String fullFilePath) throws IORuntimeException { + return writeFromStream(in, touch(fullFilePath)); + } + + /** + * 将文件写入流中,此方法不会关闭输出流 + * + * @param file 文件 + * @param out 流 + * @return 写出的流byte数 + * @throws IORuntimeException IO异常 + */ + public static long writeToStream(File file, OutputStream out) throws IORuntimeException { + return FileReader.create(file).writeToStream(out); + } + + /** + * 将路径对应文件写入流中,此方法不会关闭输出流 + * + * @param fullFilePath 文件绝对路径 + * @param out 输出流 + * @return 写出的流byte数 + * @throws IORuntimeException IO异常 + */ + public static long writeToStream(String fullFilePath, OutputStream out) throws IORuntimeException { + return writeToStream(touch(fullFilePath), out); + } + + /** + * 可读的文件大小 + * + * @param file 文件 + * @return 大小 + */ + public static String readableFileSize(File file) { + return readableFileSize(file.length()); + } + + /** + * 可读的文件大小
+ * 参考 http://stackoverflow.com/questions/3263892/format-file-size-as-mb-gb-etc + * + * @param size Long类型大小 + * @return 大小 + * @see DataSizeUtil#format(long) + */ + public static String readableFileSize(long size) { + return DataSizeUtil.format(size); + } + + /** + * 转换文件编码
+ * 此方法用于转换文件编码,读取的文件实际编码必须与指定的srcCharset编码一致,否则导致乱码 + * + * @param file 文件 + * @param srcCharset 原文件的编码,必须与文件内容的编码保持一致 + * @param destCharset 转码后的编码 + * @return 被转换编码的文件 + * @see CharsetUtil#convert(File, Charset, Charset) + * @since 3.1.0 + */ + public static File convertCharset(File file, Charset srcCharset, Charset destCharset) { + return CharsetUtil.convert(file, srcCharset, destCharset); + } + + /** + * 转换换行符
+ * 将给定文件的换行符转换为指定换行符 + * + * @param file 文件 + * @param charset 编码 + * @param lineSeparator 换行符枚举{@link LineSeparator} + * @return 被修改的文件 + * @since 3.1.0 + */ + public static File convertLineSeparator(File file, Charset charset, LineSeparator lineSeparator) { + final List lines = readLines(file, charset); + return FileWriter.create(file, charset).writeLines(lines, lineSeparator, false); + } + + /** + * 清除文件名中的在Windows下不支持的非法字符,包括: \ / : * ? " < > | + * + * @param fileName 文件名(必须不包括路径,否则路径符将被替换) + * @return 清理后的文件名 + * @see FileNameUtil#cleanInvalid(String) + * @since 3.3.1 + */ + public static String cleanInvalid(String fileName) { + return FileNameUtil.cleanInvalid(fileName); + } + + /** + * 文件名中是否包含在Windows下不支持的非法字符,包括: \ / : * ? " < > | + * + * @param fileName 文件名(必须不包括路径,否则路径符将被替换) + * @return 是否包含非法字符 + * @see FileNameUtil#containsInvalid(String) + * @since 3.3.1 + */ + public static boolean containsInvalid(String fileName) { + return FileNameUtil.containsInvalid(fileName); + } + + /** + * 计算文件CRC32校验码 + * + * @param file 文件,不能为目录 + * @return CRC32值 + * @throws IORuntimeException IO异常 + * @since 4.0.6 + */ + public static long checksumCRC32(File file) throws IORuntimeException { + return checksum(file, new CRC32()).getValue(); + } + + /** + * 计算文件校验码 + * + * @param file 文件,不能为目录 + * @param checksum {@link Checksum} + * @return Checksum + * @throws IORuntimeException IO异常 + * @since 4.0.6 + */ + public static Checksum checksum(File file, Checksum checksum) throws IORuntimeException { + Assert.notNull(file, "File is null !"); + if (file.isDirectory()) { + throw new IllegalArgumentException("Checksums can't be computed on directories"); + } + try { + return IoUtil.checksum(Files.newInputStream(file.toPath()), checksum); + } catch (IOException e) { + throw new IORuntimeException(e); + } + } + + /** + * 获取Web项目下的web root路径
+ * 原理是首先获取ClassPath路径,由于在web项目中ClassPath位于 WEB-INF/classes/下,故向上获取两级目录即可。 + * + * @return web root路径 + * @since 4.0.13 + */ + public static File getWebRoot() { + final String classPath = ClassUtil.getClassPath(); + if (StrUtil.isNotBlank(classPath)) { + return getParent(file(classPath), 2); + } + return null; + } + + /** + * 获取指定层级的父路径 + * + *
+	 * getParent("d:/aaa/bbb/cc/ddd", 0) -》 "d:/aaa/bbb/cc/ddd"
+	 * getParent("d:/aaa/bbb/cc/ddd", 2) -》 "d:/aaa/bbb"
+	 * getParent("d:/aaa/bbb/cc/ddd", 4) -》 "d:/"
+	 * getParent("d:/aaa/bbb/cc/ddd", 5) -》 null
+	 * 
+ * + * @param filePath 目录或文件路径 + * @param level 层级 + * @return 路径File,如果不存在返回null + * @since 4.1.2 + */ + public static String getParent(String filePath, int level) { + final File parent = getParent(file(filePath), level); + try { + return null == parent ? null : parent.getCanonicalPath(); + } catch (IOException e) { + throw new IORuntimeException(e); + } + } + + /** + * 获取指定层级的父路径 + * + *
+	 * getParent(file("d:/aaa/bbb/cc/ddd", 0)) -》 "d:/aaa/bbb/cc/ddd"
+	 * getParent(file("d:/aaa/bbb/cc/ddd", 2)) -》 "d:/aaa/bbb"
+	 * getParent(file("d:/aaa/bbb/cc/ddd", 4)) -》 "d:/"
+	 * getParent(file("d:/aaa/bbb/cc/ddd", 5)) -》 null
+	 * 
+ * + * @param file 目录或文件 + * @param level 层级 + * @return 路径File,如果不存在返回null + * @since 4.1.2 + */ + public static File getParent(File file, int level) { + if (level < 1 || null == file) { + return file; + } + + File parentFile; + try { + parentFile = file.getCanonicalFile().getParentFile(); + } catch (IOException e) { + throw new IORuntimeException(e); + } + if (1 == level) { + return parentFile; + } + return getParent(parentFile, level - 1); + } + + /** + * 检查父完整路径是否为自路径的前半部分,如果不是说明不是子路径,可能存在slip注入。 + *

+ * 见http://blog.nsfocus.net/zip-slip-2/ + * + * @param parentFile 父文件或目录 + * @param file 子文件或目录 + * @return 子文件或目录 + * @throws IllegalArgumentException 检查创建的子文件不在父目录中抛出此异常 + */ + public static File checkSlip(File parentFile, File file) throws IllegalArgumentException { + if (null != parentFile && null != file) { + String parentCanonicalPath; + String canonicalPath; + try { + parentCanonicalPath = parentFile.getCanonicalPath(); + canonicalPath = file.getCanonicalPath(); + } catch (IOException e) { + // issue#I4CWMO@Gitee + // getCanonicalPath有时会抛出奇怪的IO异常,此时忽略异常,使用AbsolutePath判断。 + parentCanonicalPath = parentFile.getAbsolutePath(); + canonicalPath = file.getAbsolutePath(); + } + if (!canonicalPath.startsWith(parentCanonicalPath)) { + throw new IllegalArgumentException("New file is outside of the parent dir: " + file.getName()); + } + } + return file; + } + + /** + * 根据文件扩展名获得MimeType + * + * @param filePath 文件路径或文件名 + * @return MimeType + * @since 4.1.15 + */ + public static String getMimeType(String filePath) { + String contentType = URLConnection.getFileNameMap().getContentTypeFor(filePath); + if (null == contentType) { + // 补充一些常用的mimeType + if (StrUtil.endWithIgnoreCase(filePath, ".css")) { + contentType = "text/css"; + } else if (StrUtil.endWithIgnoreCase(filePath, ".js")) { + contentType = "application/x-javascript"; + } else if (StrUtil.endWithIgnoreCase(filePath, ".rar")) { + contentType = "application/x-rar-compressed"; + } else if (StrUtil.endWithIgnoreCase(filePath, ".7z")) { + contentType = "application/x-7z-compressed"; + } else if (StrUtil.endWithIgnoreCase(filePath, ".wgt")) { + contentType = "application/widget"; + } + } + + // 补充 + if (null == contentType) { + contentType = getMimeType(Paths.get(filePath)); + } + + return contentType; + } + + /** + * 判断是否为符号链接文件 + * + * @param file 被检查的文件 + * @return 是否为符号链接文件 + * @since 4.4.2 + */ + public static boolean isSymlink(File file) { + return isSymlink(file.toPath()); + } + + /** + * 判断给定的目录是否为给定文件或文件夹的子目录 + * + * @param parent 父目录 + * @param sub 子目录 + * @return 子目录是否为父目录的子目录 + * @since 4.5.4 + */ + public static boolean isSub(File parent, File sub) { + Assert.notNull(parent); + Assert.notNull(sub); + return isSub(parent.toPath(), sub.toPath()); + } + + /** + * 创建{@link RandomAccessFile} + * + * @param path 文件Path + * @param mode 模式,见{@link FileMode} + * @return {@link RandomAccessFile} + * @since 4.5.2 + */ + public static RandomAccessFile createRandomAccessFile(Path path, FileMode mode) { + return createRandomAccessFile(path.toFile(), mode); + } + + /** + * 创建{@link RandomAccessFile} + * + * @param file 文件 + * @param mode 模式,见{@link FileMode} + * @return {@link RandomAccessFile} + * @since 4.5.2 + */ + public static RandomAccessFile createRandomAccessFile(File file, FileMode mode) { + try { + return new RandomAccessFile(file, mode.name()); + } catch (FileNotFoundException e) { + throw new IORuntimeException(e); + } + } + + /** + * 文件内容跟随器,实现类似Linux下"tail -f"命令功能
+ * 此方法会阻塞当前线程 + * + * @param file 文件 + * @param handler 行处理器 + */ + public static void tail(File file, LineHandler handler) { + tail(file, CharsetUtil.CHARSET_UTF_8, handler); + } + + /** + * 文件内容跟随器,实现类似Linux下"tail -f"命令功能
+ * 此方法会阻塞当前线程 + * + * @param file 文件 + * @param charset 编码 + * @param handler 行处理器 + */ + public static void tail(File file, Charset charset, LineHandler handler) { + new Tailer(file, charset, handler).start(); + } + + /** + * 文件内容跟随器,实现类似Linux下"tail -f"命令功能
+ * 此方法会阻塞当前线程 + * + * @param file 文件 + * @param charset 编码 + */ + public static void tail(File file, Charset charset) { + tail(file, charset, Tailer.CONSOLE_HANDLER); + } + + /** + * 根据压缩包中的路径构建目录结构,在Win下直接构建,在Linux下拆分路径单独构建 + * + * @param outFile 最外部路径 + * @param fileName 文件名,可以包含路径 + * @return 文件或目录 + * @since 5.0.5 + */ + private static File buildFile(File outFile, String fileName) { + // 替换Windows路径分隔符为Linux路径分隔符,便于统一处理 + fileName = fileName.replace('\\', '/'); + if (!isWindows() + // 检查文件名中是否包含"/",不考虑以"/"结尾的情况 + && fileName.lastIndexOf(CharUtil.SLASH, fileName.length() - 2) > 0) { + // 在Linux下多层目录创建存在问题,/会被当成文件名的一部分,此处做处理 + // 使用/拆分路径(zip中无\),级联创建父目录 + final List pathParts = StrUtil.split(fileName, '/', false, true); + final int lastPartIndex = pathParts.size() - 1;//目录个数 + for (int i = 0; i < lastPartIndex; i++) { + //由于路径拆分,slip不检查,在最后一步检查 + outFile = new File(outFile, pathParts.get(i)); + } + //noinspection ResultOfMethodCallIgnored + outFile.mkdirs(); + // 最后一个部分如果非空,作为文件名 + fileName = pathParts.get(lastPartIndex); + } + return new File(outFile, fileName); + } +} diff --git a/src/main/java/cn/hutool/core/io/IORuntimeException.java b/src/main/java/cn/hutool/core/io/IORuntimeException.java new file mode 100644 index 0000000..e6f9645 --- /dev/null +++ b/src/main/java/cn/hutool/core/io/IORuntimeException.java @@ -0,0 +1,44 @@ +package cn.hutool.core.io; + +import cn.hutool.core.exceptions.ExceptionUtil; +import cn.hutool.core.util.StrUtil; + +/** + * IO运行时异常,常用于对IOException的包装 + * + * @author xiaoleilu + */ +public class IORuntimeException extends RuntimeException { + private static final long serialVersionUID = 8247610319171014183L; + + public IORuntimeException(Throwable e) { + super(ExceptionUtil.getMessage(e), e); + } + + public IORuntimeException(String message) { + super(message); + } + + public IORuntimeException(String messageTemplate, Object... params) { + super(StrUtil.format(messageTemplate, params)); + } + + public IORuntimeException(String message, Throwable throwable) { + super(message, throwable); + } + + public IORuntimeException(Throwable throwable, String messageTemplate, Object... params) { + super(StrUtil.format(messageTemplate, params), throwable); + } + + /** + * 导致这个异常的异常是否是指定类型的异常 + * + * @param clazz 异常类 + * @return 是否为指定类型异常 + */ + public boolean causeInstanceOf(Class clazz) { + final Throwable cause = this.getCause(); + return null != clazz && clazz.isInstance(cause); + } +} diff --git a/src/main/java/cn/hutool/core/io/IoUtil.java b/src/main/java/cn/hutool/core/io/IoUtil.java new file mode 100644 index 0000000..fbf24a6 --- /dev/null +++ b/src/main/java/cn/hutool/core/io/IoUtil.java @@ -0,0 +1,1332 @@ +package cn.hutool.core.io; + +import cn.hutool.core.collection.LineIter; +import cn.hutool.core.convert.Convert; +import cn.hutool.core.exceptions.UtilException; +import cn.hutool.core.io.copy.ReaderWriterCopier; +import cn.hutool.core.io.copy.StreamCopier; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.CharsetUtil; +import cn.hutool.core.util.HexUtil; +import cn.hutool.core.util.StrUtil; + +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.Closeable; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.Flushable; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.ObjectOutputStream; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.PushbackInputStream; +import java.io.PushbackReader; +import java.io.Reader; +import java.io.Serializable; +import java.io.Writer; +import java.nio.CharBuffer; +import java.nio.channels.FileChannel; +import java.nio.charset.Charset; +import java.util.Collection; +import java.util.Objects; +import java.util.zip.CRC32; +import java.util.zip.CheckedInputStream; +import java.util.zip.Checksum; + +/** + * IO工具类
+ * IO工具类只是辅助流的读写,并不负责关闭流。原因是流可能被多次读写,读写关闭后容易造成问题。 + * + * @author xiaoleilu + */ +public class IoUtil extends NioUtil { + + // -------------------------------------------------------------------------------------- Copy start + + /** + * 将Reader中的内容复制到Writer中 使用默认缓存大小,拷贝后不关闭Reader + * + * @param reader Reader + * @param writer Writer + * @return 拷贝的字节数 + * @throws IORuntimeException IO异常 + */ + public static long copy(Reader reader, Writer writer) throws IORuntimeException { + return copy(reader, writer, DEFAULT_BUFFER_SIZE); + } + + /** + * 将Reader中的内容复制到Writer中,拷贝后不关闭Reader + * + * @param reader Reader + * @param writer Writer + * @param bufferSize 缓存大小 + * @return 传输的byte数 + * @throws IORuntimeException IO异常 + */ + public static long copy(Reader reader, Writer writer, int bufferSize) throws IORuntimeException { + return copy(reader, writer, bufferSize, null); + } + + /** + * 将Reader中的内容复制到Writer中,拷贝后不关闭Reader + * + * @param reader Reader + * @param writer Writer + * @param bufferSize 缓存大小 + * @param streamProgress 进度处理器 + * @return 传输的byte数 + * @throws IORuntimeException IO异常 + */ + public static long copy(Reader reader, Writer writer, int bufferSize, StreamProgress streamProgress) throws IORuntimeException { + return copy(reader, writer, bufferSize, -1, streamProgress); + } + + /** + * 将Reader中的内容复制到Writer中,拷贝后不关闭Reader + * + * @param reader Reader + * @param writer Writer + * @param bufferSize 缓存大小 + * @param count 最大长度 + * @param streamProgress 进度处理器 + * @return 传输的byte数 + * @throws IORuntimeException IO异常 + */ + public static long copy(Reader reader, Writer writer, int bufferSize, long count, StreamProgress streamProgress) throws IORuntimeException { + return new ReaderWriterCopier(bufferSize, count, streamProgress).copy(reader, writer); + } + + /** + * 拷贝流,使用默认Buffer大小,拷贝后不关闭流 + * + * @param in 输入流 + * @param out 输出流 + * @return 传输的byte数 + * @throws IORuntimeException IO异常 + */ + public static long copy(InputStream in, OutputStream out) throws IORuntimeException { + return copy(in, out, DEFAULT_BUFFER_SIZE); + } + + /** + * 拷贝流,拷贝后不关闭流 + * + * @param in 输入流 + * @param out 输出流 + * @param bufferSize 缓存大小 + * @return 传输的byte数 + * @throws IORuntimeException IO异常 + */ + public static long copy(InputStream in, OutputStream out, int bufferSize) throws IORuntimeException { + return copy(in, out, bufferSize, null); + } + + /** + * 拷贝流,拷贝后不关闭流 + * + * @param in 输入流 + * @param out 输出流 + * @param bufferSize 缓存大小 + * @param streamProgress 进度条 + * @return 传输的byte数 + * @throws IORuntimeException IO异常 + */ + public static long copy(InputStream in, OutputStream out, int bufferSize, StreamProgress streamProgress) throws IORuntimeException { + return copy(in, out, bufferSize, -1, streamProgress); + } + + /** + * 拷贝流,拷贝后不关闭流 + * + * @param in 输入流 + * @param out 输出流 + * @param bufferSize 缓存大小 + * @param count 总拷贝长度 + * @param streamProgress 进度条 + * @return 传输的byte数 + * @throws IORuntimeException IO异常 + * @since 5.7.8 + */ + public static long copy(InputStream in, OutputStream out, int bufferSize, long count, StreamProgress streamProgress) throws IORuntimeException { + return new StreamCopier(bufferSize, count, streamProgress).copy(in, out); + } + + /** + * 拷贝文件流,使用NIO + * + * @param in 输入 + * @param out 输出 + * @return 拷贝的字节数 + * @throws IORuntimeException IO异常 + */ + public static long copy(FileInputStream in, FileOutputStream out) throws IORuntimeException { + Assert.notNull(in, "FileInputStream is null!"); + Assert.notNull(out, "FileOutputStream is null!"); + + FileChannel inChannel = null; + FileChannel outChannel = null; + try { + inChannel = in.getChannel(); + outChannel = out.getChannel(); + return copy(inChannel, outChannel); + } finally { + close(outChannel); + close(inChannel); + } + } + + // -------------------------------------------------------------------------------------- Copy end + + // -------------------------------------------------------------------------------------- getReader and getWriter start + + /** + * 获得一个文件读取器,默认使用UTF-8编码 + * + * @param in 输入流 + * @return BufferedReader对象 + * @since 5.1.6 + */ + public static BufferedReader getUtf8Reader(InputStream in) { + return getReader(in, CharsetUtil.CHARSET_UTF_8); + } + + /** + * 获得一个文件读取器 + * + * @param in 输入流 + * @param charsetName 字符集名称 + * @return BufferedReader对象 + * @deprecated 请使用 {@link #getReader(InputStream, Charset)} + */ + @Deprecated + public static BufferedReader getReader(InputStream in, String charsetName) { + return getReader(in, Charset.forName(charsetName)); + } + + /** + * 从{@link BOMInputStream}中获取Reader + * + * @param in {@link BOMInputStream} + * @return {@link BufferedReader} + * @since 5.5.8 + */ + public static BufferedReader getReader(BOMInputStream in) { + return getReader(in, in.getCharset()); + } + + /** + * 从{@link InputStream}中获取{@link BomReader} + * + * @param in {@link InputStream} + * @return {@link BomReader} + * @since 5.7.14 + */ + public static BomReader getBomReader(InputStream in) { + return new BomReader(in); + } + + /** + * 获得一个Reader + * + * @param in 输入流 + * @param charset 字符集 + * @return BufferedReader对象 + */ + public static BufferedReader getReader(InputStream in, Charset charset) { + if (null == in) { + return null; + } + + InputStreamReader reader; + if (null == charset) { + reader = new InputStreamReader(in); + } else { + reader = new InputStreamReader(in, charset); + } + + return new BufferedReader(reader); + } + + /** + * 获得{@link BufferedReader}
+ * 如果是{@link BufferedReader}强转返回,否则新建。如果提供的Reader为null返回null + * + * @param reader 普通Reader,如果为null返回null + * @return {@link BufferedReader} or null + * @since 3.0.9 + */ + public static BufferedReader getReader(Reader reader) { + if (null == reader) { + return null; + } + + return (reader instanceof BufferedReader) ? (BufferedReader) reader : new BufferedReader(reader); + } + + /** + * 获得{@link PushbackReader}
+ * 如果是{@link PushbackReader}强转返回,否则新建 + * + * @param reader 普通Reader + * @param pushBackSize 推后的byte数 + * @return {@link PushbackReader} + * @since 3.1.0 + */ + public static PushbackReader getPushBackReader(Reader reader, int pushBackSize) { + return (reader instanceof PushbackReader) ? (PushbackReader) reader : new PushbackReader(reader, pushBackSize); + } + + /** + * 获得一个Writer,默认编码UTF-8 + * + * @param out 输入流 + * @return OutputStreamWriter对象 + * @since 5.1.6 + */ + public static OutputStreamWriter getUtf8Writer(OutputStream out) { + return getWriter(out, CharsetUtil.CHARSET_UTF_8); + } + + /** + * 获得一个Writer + * + * @param out 输入流 + * @param charsetName 字符集 + * @return OutputStreamWriter对象 + * @deprecated 请使用 {@link #getWriter(OutputStream, Charset)} + */ + @Deprecated + public static OutputStreamWriter getWriter(OutputStream out, String charsetName) { + return getWriter(out, Charset.forName(charsetName)); + } + + /** + * 获得一个Writer + * + * @param out 输入流 + * @param charset 字符集 + * @return OutputStreamWriter对象 + */ + public static OutputStreamWriter getWriter(OutputStream out, Charset charset) { + if (null == out) { + return null; + } + + if (null == charset) { + return new OutputStreamWriter(out); + } else { + return new OutputStreamWriter(out, charset); + } + } + // -------------------------------------------------------------------------------------- getReader and getWriter end + + // -------------------------------------------------------------------------------------- read start + + /** + * 从流中读取UTF8编码的内容 + * + * @param in 输入流 + * @return 内容 + * @throws IORuntimeException IO异常 + * @since 5.4.4 + */ + public static String readUtf8(InputStream in) throws IORuntimeException { + return read(in, CharsetUtil.CHARSET_UTF_8); + } + + /** + * 从流中读取内容,读取完成后关闭流 + * + * @param in 输入流 + * @param charsetName 字符集 + * @return 内容 + * @throws IORuntimeException IO异常 + * @deprecated 请使用 {@link #read(InputStream, Charset)} + */ + @Deprecated + public static String read(InputStream in, String charsetName) throws IORuntimeException { + final FastByteArrayOutputStream out = read(in); + return StrUtil.isBlank(charsetName) ? out.toString() : out.toString(charsetName); + } + + /** + * 从流中读取内容,读取完毕后关闭流 + * + * @param in 输入流,读取完毕后关闭流 + * @param charset 字符集 + * @return 内容 + * @throws IORuntimeException IO异常 + */ + public static String read(InputStream in, Charset charset) throws IORuntimeException { + return StrUtil.str(readBytes(in), charset); + } + + /** + * 从流中读取内容,读到输出流中,读取完毕后关闭流 + * + * @param in 输入流 + * @return 输出流 + * @throws IORuntimeException IO异常 + */ + public static FastByteArrayOutputStream read(InputStream in) throws IORuntimeException { + return read(in, true); + } + + /** + * 从流中读取内容,读到输出流中,读取完毕后可选是否关闭流 + * + * @param in 输入流 + * @param isClose 读取完毕后是否关闭流 + * @return 输出流 + * @throws IORuntimeException IO异常 + * @since 5.5.3 + */ + public static FastByteArrayOutputStream read(InputStream in, boolean isClose) throws IORuntimeException { + final FastByteArrayOutputStream out; + if (in instanceof FileInputStream) { + // 文件流的长度是可预见的,此时直接读取效率更高 + try { + out = new FastByteArrayOutputStream(in.available()); + } catch (IOException e) { + throw new IORuntimeException(e); + } + } else { + out = new FastByteArrayOutputStream(); + } + try { + copy(in, out); + } finally { + if (isClose) { + close(in); + } + } + return out; + } + + /** + * 从Reader中读取String,读取完毕后关闭Reader + * + * @param reader Reader + * @return String + * @throws IORuntimeException IO异常 + */ + public static String read(Reader reader) throws IORuntimeException { + return read(reader, true); + } + + /** + * 从{@link Reader}中读取String + * + * @param reader {@link Reader} + * @param isClose 是否关闭{@link Reader} + * @return String + * @throws IORuntimeException IO异常 + */ + public static String read(Reader reader, boolean isClose) throws IORuntimeException { + final StringBuilder builder = StrUtil.builder(); + final CharBuffer buffer = CharBuffer.allocate(DEFAULT_BUFFER_SIZE); + try { + while (-1 != reader.read(buffer)) { + builder.append(buffer.flip()); + } + } catch (IOException e) { + throw new IORuntimeException(e); + } finally { + if (isClose) { + IoUtil.close(reader); + } + } + return builder.toString(); + } + + /** + * 从流中读取bytes,读取完毕后关闭流 + * + * @param in {@link InputStream} + * @return bytes + * @throws IORuntimeException IO异常 + */ + public static byte[] readBytes(InputStream in) throws IORuntimeException { + return readBytes(in, true); + } + + /** + * 从流中读取bytes + * + * @param in {@link InputStream} + * @param isClose 是否关闭输入流 + * @return bytes + * @throws IORuntimeException IO异常 + * @since 5.0.4 + */ + public static byte[] readBytes(InputStream in, boolean isClose) throws IORuntimeException { + return read(in, isClose).toByteArray(); + } + + /** + * 读取指定长度的byte数组,不关闭流 + * + * @param in {@link InputStream},为{@code null}返回{@code null} + * @param length 长度,小于等于0返回空byte数组 + * @return bytes + * @throws IORuntimeException IO异常 + */ + public static byte[] readBytes(InputStream in, int length) throws IORuntimeException { + if (null == in) { + return null; + } + if (length <= 0) { + return new byte[0]; + } + + final FastByteArrayOutputStream out = new FastByteArrayOutputStream(length); + copy(in, out, DEFAULT_BUFFER_SIZE, length, null); + return out.toByteArray(); + } + + /** + * 读取16进制字符串 + * + * @param in {@link InputStream} + * @param length 长度 + * @param toLowerCase true 传换成小写格式 , false 传换成大写格式 + * @return 16进制字符串 + * @throws IORuntimeException IO异常 + */ + public static String readHex(InputStream in, int length, boolean toLowerCase) throws IORuntimeException { + return HexUtil.encodeHexStr(readBytes(in, length), toLowerCase); + } + + /** + * 从流中读取前64个byte并转换为16进制,字母部分使用大写 + * + * @param in {@link InputStream} + * @return 16进制字符串 + * @throws IORuntimeException IO异常 + */ + public static String readHex64Upper(InputStream in) throws IORuntimeException { + return readHex(in, 64, false); + } + + /** + * 从流中读取前8192个byte并转换为16进制,字母部分使用大写 + * + * @param in {@link InputStream} + * @return 16进制字符串 + * @throws IORuntimeException IO异常 + */ + public static String readHex8192Upper(InputStream in) throws IORuntimeException { + try { + int i = in.available(); + return readHex(in, Math.min(8192, in.available()), false); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + /** + * 从流中读取前64个byte并转换为16进制,字母部分使用小写 + * + * @param in {@link InputStream} + * @return 16进制字符串 + * @throws IORuntimeException IO异常 + */ + public static String readHex64Lower(InputStream in) throws IORuntimeException { + return readHex(in, 64, true); + } + + /** + * 从流中读取对象,即对象的反序列化 + * + *

+ * 注意!!! 此方法不会检查反序列化安全,可能存在反序列化漏洞风险!!! + *

+ * + * @param 读取对象的类型 + * @param in 输入流 + * @return 输出流 + * @throws IORuntimeException IO异常 + * @throws UtilException ClassNotFoundException包装 + */ + public static T readObj(InputStream in) throws IORuntimeException, UtilException { + return readObj(in, null); + } + + /** + * 从流中读取对象,即对象的反序列化,读取后不关闭流 + * + *

+ * 注意!!! 此方法不会检查反序列化安全,可能存在反序列化漏洞风险!!! + *

+ * + * @param 读取对象的类型 + * @param in 输入流 + * @param clazz 读取对象类型 + * @return 输出流 + * @throws IORuntimeException IO异常 + * @throws UtilException ClassNotFoundException包装 + */ + public static T readObj(InputStream in, Class clazz) throws IORuntimeException, UtilException { + try { + return readObj((in instanceof ValidateObjectInputStream) ? + (ValidateObjectInputStream) in : new ValidateObjectInputStream(in), + clazz); + } catch (IOException e) { + throw new IORuntimeException(e); + } + } + + /** + * 从流中读取对象,即对象的反序列化,读取后不关闭流 + * + *

+ * 此方法使用了{@link ValidateObjectInputStream}中的黑白名单方式过滤类,用于避免反序列化漏洞
+ * 通过构造{@link ValidateObjectInputStream},调用{@link ValidateObjectInputStream#accept(Class[])} + * 或者{@link ValidateObjectInputStream#refuse(Class[])}方法添加可以被序列化的类或者禁止序列化的类。 + *

+ * + * @param 读取对象的类型 + * @param in 输入流,使用{@link ValidateObjectInputStream}中的黑白名单方式过滤类,用于避免反序列化漏洞 + * @param clazz 读取对象类型 + * @return 输出流 + * @throws IORuntimeException IO异常 + * @throws UtilException ClassNotFoundException包装 + */ + public static T readObj(ValidateObjectInputStream in, Class clazz) throws IORuntimeException, UtilException { + if (in == null) { + throw new IllegalArgumentException("The InputStream must not be null"); + } + if(null != clazz){ + in.accept(clazz); + } + try { + //noinspection unchecked + return (T) in.readObject(); + } catch (IOException e) { + throw new IORuntimeException(e); + } catch (ClassNotFoundException e) { + throw new UtilException(e); + } + } + + /** + * 从流中读取内容,使用UTF-8编码 + * + * @param 集合类型 + * @param in 输入流 + * @param collection 返回集合 + * @return 内容 + * @throws IORuntimeException IO异常 + */ + public static > T readUtf8Lines(InputStream in, T collection) throws IORuntimeException { + return readLines(in, CharsetUtil.CHARSET_UTF_8, collection); + } + + /** + * 从流中读取内容 + * + * @param 集合类型 + * @param in 输入流 + * @param charsetName 字符集 + * @param collection 返回集合 + * @return 内容 + * @throws IORuntimeException IO异常 + * @deprecated 请使用 {@link #readLines(InputStream, Charset, Collection)} + */ + @Deprecated + public static > T readLines(InputStream in, String charsetName, T collection) throws IORuntimeException { + return readLines(in, CharsetUtil.charset(charsetName), collection); + } + + /** + * 从流中读取内容 + * + * @param 集合类型 + * @param in 输入流 + * @param charset 字符集 + * @param collection 返回集合 + * @return 内容 + * @throws IORuntimeException IO异常 + */ + public static > T readLines(InputStream in, Charset charset, T collection) throws IORuntimeException { + return readLines(getReader(in, charset), collection); + } + + /** + * 从Reader中读取内容 + * + * @param 集合类型 + * @param reader {@link Reader} + * @param collection 返回集合 + * @return 内容 + * @throws IORuntimeException IO异常 + */ + public static > T readLines(Reader reader, T collection) throws IORuntimeException { + readLines(reader, (LineHandler) collection::add); + return collection; + } + + /** + * 按行读取UTF-8编码数据,针对每行的数据做处理 + * + * @param in {@link InputStream} + * @param lineHandler 行处理接口,实现handle方法用于编辑一行的数据后入到指定地方 + * @throws IORuntimeException IO异常 + * @since 3.1.1 + */ + public static void readUtf8Lines(InputStream in, LineHandler lineHandler) throws IORuntimeException { + readLines(in, CharsetUtil.CHARSET_UTF_8, lineHandler); + } + + /** + * 按行读取数据,针对每行的数据做处理 + * + * @param in {@link InputStream} + * @param charset {@link Charset}编码 + * @param lineHandler 行处理接口,实现handle方法用于编辑一行的数据后入到指定地方 + * @throws IORuntimeException IO异常 + * @since 3.0.9 + */ + public static void readLines(InputStream in, Charset charset, LineHandler lineHandler) throws IORuntimeException { + readLines(getReader(in, charset), lineHandler); + } + + /** + * 按行读取数据,针对每行的数据做处理
+ * {@link Reader}自带编码定义,因此读取数据的编码跟随其编码。
+ * 此方法不会关闭流,除非抛出异常 + * + * @param reader {@link Reader} + * @param lineHandler 行处理接口,实现handle方法用于编辑一行的数据后入到指定地方 + * @throws IORuntimeException IO异常 + */ + public static void readLines(Reader reader, LineHandler lineHandler) throws IORuntimeException { + Assert.notNull(reader); + Assert.notNull(lineHandler); + + for (String line : lineIter(reader)) { + lineHandler.handle(line); + } + } + + // -------------------------------------------------------------------------------------- read end + + /** + * String 转为流 + * + * @param content 内容 + * @param charsetName 编码 + * @return 字节流 + * @deprecated 请使用 {@link #toStream(String, Charset)} + */ + @Deprecated + public static ByteArrayInputStream toStream(String content, String charsetName) { + return toStream(content, CharsetUtil.charset(charsetName)); + } + + /** + * String 转为流 + * + * @param content 内容 + * @param charset 编码 + * @return 字节流 + */ + public static ByteArrayInputStream toStream(String content, Charset charset) { + if (content == null) { + return null; + } + return toStream(StrUtil.bytes(content, charset)); + } + + /** + * String 转为UTF-8编码的字节流流 + * + * @param content 内容 + * @return 字节流 + * @since 4.5.1 + */ + public static ByteArrayInputStream toUtf8Stream(String content) { + return toStream(content, CharsetUtil.CHARSET_UTF_8); + } + + /** + * 文件转为{@link FileInputStream} + * + * @param file 文件 + * @return {@link FileInputStream} + */ + public static FileInputStream toStream(File file) { + try { + return new FileInputStream(file); + } catch (FileNotFoundException e) { + throw new IORuntimeException(e); + } + } + + /** + * byte[] 转为{@link ByteArrayInputStream} + * + * @param content 内容bytes + * @return 字节流 + * @since 4.1.8 + */ + public static ByteArrayInputStream toStream(byte[] content) { + if (content == null) { + return null; + } + return new ByteArrayInputStream(content); + } + + /** + * {@link ByteArrayOutputStream}转为{@link ByteArrayInputStream} + * + * @param out {@link ByteArrayOutputStream} + * @return 字节流 + * @since 5.3.6 + */ + public static ByteArrayInputStream toStream(ByteArrayOutputStream out) { + if (out == null) { + return null; + } + return new ByteArrayInputStream(out.toByteArray()); + } + + /** + * 转换为{@link BufferedInputStream} + * + * @param in {@link InputStream} + * @return {@link BufferedInputStream} + * @since 4.0.10 + */ + public static BufferedInputStream toBuffered(InputStream in) { + Assert.notNull(in, "InputStream must be not null!"); + return (in instanceof BufferedInputStream) ? (BufferedInputStream) in : new BufferedInputStream(in); + } + + /** + * 转换为{@link BufferedInputStream} + * + * @param in {@link InputStream} + * @param bufferSize buffer size + * @return {@link BufferedInputStream} + * @since 5.6.1 + */ + public static BufferedInputStream toBuffered(InputStream in, int bufferSize) { + Assert.notNull(in, "InputStream must be not null!"); + return (in instanceof BufferedInputStream) ? (BufferedInputStream) in : new BufferedInputStream(in, bufferSize); + } + + /** + * 转换为{@link BufferedOutputStream} + * + * @param out {@link OutputStream} + * @return {@link BufferedOutputStream} + * @since 4.0.10 + */ + public static BufferedOutputStream toBuffered(OutputStream out) { + Assert.notNull(out, "OutputStream must be not null!"); + return (out instanceof BufferedOutputStream) ? (BufferedOutputStream) out : new BufferedOutputStream(out); + } + + /** + * 转换为{@link BufferedOutputStream} + * + * @param out {@link OutputStream} + * @param bufferSize buffer size + * @return {@link BufferedOutputStream} + * @since 5.6.1 + */ + public static BufferedOutputStream toBuffered(OutputStream out, int bufferSize) { + Assert.notNull(out, "OutputStream must be not null!"); + return (out instanceof BufferedOutputStream) ? (BufferedOutputStream) out : new BufferedOutputStream(out, bufferSize); + } + + /** + * 转换为{@link BufferedReader} + * + * @param reader {@link Reader} + * @return {@link BufferedReader} + * @since 5.6.1 + */ + public static BufferedReader toBuffered(Reader reader) { + Assert.notNull(reader, "Reader must be not null!"); + return (reader instanceof BufferedReader) ? (BufferedReader) reader : new BufferedReader(reader); + } + + /** + * 转换为{@link BufferedReader} + * + * @param reader {@link Reader} + * @param bufferSize buffer size + * @return {@link BufferedReader} + * @since 5.6.1 + */ + public static BufferedReader toBuffered(Reader reader, int bufferSize) { + Assert.notNull(reader, "Reader must be not null!"); + return (reader instanceof BufferedReader) ? (BufferedReader) reader : new BufferedReader(reader, bufferSize); + } + + /** + * 转换为{@link BufferedWriter} + * + * @param writer {@link Writer} + * @return {@link BufferedWriter} + * @since 5.6.1 + */ + public static BufferedWriter toBuffered(Writer writer) { + Assert.notNull(writer, "Writer must be not null!"); + return (writer instanceof BufferedWriter) ? (BufferedWriter) writer : new BufferedWriter(writer); + } + + /** + * 转换为{@link BufferedWriter} + * + * @param writer {@link Writer} + * @param bufferSize buffer size + * @return {@link BufferedWriter} + * @since 5.6.1 + */ + public static BufferedWriter toBuffered(Writer writer, int bufferSize) { + Assert.notNull(writer, "Writer must be not null!"); + return (writer instanceof BufferedWriter) ? (BufferedWriter) writer : new BufferedWriter(writer, bufferSize); + } + + /** + * 将{@link InputStream}转换为支持mark标记的流
+ * 若原流支持mark标记,则返回原流,否则使用{@link BufferedInputStream} 包装之 + * + * @param in 流 + * @return {@link InputStream} + * @since 4.0.9 + */ + public static InputStream toMarkSupportStream(InputStream in) { + if (null == in) { + return null; + } + if (!in.markSupported()) { + return new BufferedInputStream(in); + } + return in; + } + + /** + * 转换为{@link PushbackInputStream}
+ * 如果传入的输入流已经是{@link PushbackInputStream},强转返回,否则新建一个 + * + * @param in {@link InputStream} + * @param pushBackSize 推后的byte数 + * @return {@link PushbackInputStream} + * @since 3.1.0 + */ + public static PushbackInputStream toPushbackStream(InputStream in, int pushBackSize) { + return (in instanceof PushbackInputStream) ? (PushbackInputStream) in : new PushbackInputStream(in, pushBackSize); + } + + /** + * 将指定{@link InputStream} 转换为{@link InputStream#available()}方法可用的流。
+ * 在Socket通信流中,服务端未返回数据情况下{@link InputStream#available()}方法始终为{@code 0}
+ * 因此,在读取前需要调用{@link InputStream#read()}读取一个字节(未返回会阻塞),一旦读取到了,{@link InputStream#available()}方法就正常了。
+ * 需要注意的是,在网络流中,是按照块来传输的,所以 {@link InputStream#available()} 读取到的并非最终长度,而是此次块的长度。
+ * 此方法返回对象的规则为: + * + *
    + *
  • FileInputStream 返回原对象,因为文件流的available方法本身可用
  • + *
  • 其它InputStream 返回PushbackInputStream
  • + *
+ * + * @param in 被转换的流 + * @return 转换后的流,可能为{@link PushbackInputStream} + * @since 5.5.3 + */ + public static InputStream toAvailableStream(InputStream in) { + if (in instanceof FileInputStream) { + // FileInputStream本身支持available方法。 + return in; + } + + final PushbackInputStream pushbackInputStream = toPushbackStream(in, 1); + try { + final int available = pushbackInputStream.available(); + if (available <= 0) { + //此操作会阻塞,直到有数据被读到 + int b = pushbackInputStream.read(); + pushbackInputStream.unread(b); + } + } catch (IOException e) { + throw new IORuntimeException(e); + } + + return pushbackInputStream; + } + + /** + * 将byte[]写到流中 + * + * @param out 输出流 + * @param isCloseOut 写入完毕是否关闭输出流 + * @param content 写入的内容 + * @throws IORuntimeException IO异常 + */ + public static void write(OutputStream out, boolean isCloseOut, byte[] content) throws IORuntimeException { + try { + out.write(content); + } catch (IOException e) { + throw new IORuntimeException(e); + } finally { + if (isCloseOut) { + close(out); + } + } + } + + /** + * 将多部分内容写到流中,自动转换为UTF-8字符串 + * + * @param out 输出流 + * @param isCloseOut 写入完毕是否关闭输出流 + * @param contents 写入的内容,调用toString()方法,不包括不会自动换行 + * @throws IORuntimeException IO异常 + * @since 3.1.1 + */ + public static void writeUtf8(OutputStream out, boolean isCloseOut, Object... contents) throws IORuntimeException { + write(out, CharsetUtil.CHARSET_UTF_8, isCloseOut, contents); + } + + /** + * 将多部分内容写到流中,自动转换为字符串 + * + * @param out 输出流 + * @param charsetName 写出的内容的字符集 + * @param isCloseOut 写入完毕是否关闭输出流 + * @param contents 写入的内容,调用toString()方法,不包括不会自动换行 + * @throws IORuntimeException IO异常 + * @deprecated 请使用 {@link #write(OutputStream, Charset, boolean, Object...)} + */ + @Deprecated + public static void write(OutputStream out, String charsetName, boolean isCloseOut, Object... contents) throws IORuntimeException { + write(out, CharsetUtil.charset(charsetName), isCloseOut, contents); + } + + /** + * 将多部分内容写到流中,自动转换为字符串 + * + * @param out 输出流 + * @param charset 写出的内容的字符集 + * @param isCloseOut 写入完毕是否关闭输出流 + * @param contents 写入的内容,调用toString()方法,不包括不会自动换行 + * @throws IORuntimeException IO异常 + * @since 3.0.9 + */ + public static void write(OutputStream out, Charset charset, boolean isCloseOut, Object... contents) throws IORuntimeException { + OutputStreamWriter osw = null; + try { + osw = getWriter(out, charset); + for (Object content : contents) { + if (content != null) { + osw.write(Convert.toStr(content, StrUtil.EMPTY)); + } + } + osw.flush(); + } catch (IOException e) { + throw new IORuntimeException(e); + } finally { + if (isCloseOut) { + close(osw); + } + } + } + + /** + * 将多部分内容写到流中 + * + * @param out 输出流 + * @param isCloseOut 写入完毕是否关闭输出流 + * @param obj 写入的对象内容 + * @throws IORuntimeException IO异常 + * @since 5.3.3 + */ + public static void writeObj(OutputStream out, boolean isCloseOut, Serializable obj) throws IORuntimeException { + writeObjects(out, isCloseOut, obj); + } + + /** + * 将多部分内容写到流中 + * + * @param out 输出流 + * @param isCloseOut 写入完毕是否关闭输出流 + * @param contents 写入的内容 + * @throws IORuntimeException IO异常 + */ + public static void writeObjects(OutputStream out, boolean isCloseOut, Serializable... contents) throws IORuntimeException { + ObjectOutputStream osw = null; + try { + osw = out instanceof ObjectOutputStream ? (ObjectOutputStream) out : new ObjectOutputStream(out); + for (Object content : contents) { + if (content != null) { + osw.writeObject(content); + } + } + osw.flush(); + } catch (IOException e) { + throw new IORuntimeException(e); + } finally { + if (isCloseOut) { + close(osw); + } + } + } + + /** + * 从缓存中刷出数据 + * + * @param flushable {@link Flushable} + * @since 4.2.2 + */ + public static void flush(Flushable flushable) { + if (null != flushable) { + try { + flushable.flush(); + } catch (Exception e) { + // 静默刷出 + } + } + } + + /** + * 关闭
+ * 关闭失败不会抛出异常 + * + * @param closeable 被关闭的对象 + */ + public static void close(Closeable closeable) { + if (null != closeable) { + try { + closeable.close(); + } catch (Exception e) { + // 静默关闭 + } + } + } + + /** + * 尝试关闭指定对象
+ * 判断对象如果实现了{@link AutoCloseable},则调用之 + * + * @param obj 可关闭对象 + * @since 4.3.2 + */ + public static void closeIfPosible(Object obj) { + if (obj instanceof AutoCloseable) { + close((AutoCloseable) obj); + } + } + + /** + * 对比两个流内容是否相同
+ * 内部会转换流为 {@link BufferedInputStream} + * + * @param input1 第一个流 + * @param input2 第二个流 + * @return 两个流的内容一致返回true,否则false + * @throws IORuntimeException IO异常 + * @since 4.0.6 + */ + public static boolean contentEquals(InputStream input1, InputStream input2) throws IORuntimeException { + if (!(input1 instanceof BufferedInputStream)) { + input1 = new BufferedInputStream(input1); + } + if (!(input2 instanceof BufferedInputStream)) { + input2 = new BufferedInputStream(input2); + } + + try { + int ch = input1.read(); + while (EOF != ch) { + int ch2 = input2.read(); + if (ch != ch2) { + return false; + } + ch = input1.read(); + } + + int ch2 = input2.read(); + return ch2 == EOF; + } catch (IOException e) { + throw new IORuntimeException(e); + } + } + + /** + * 对比两个Reader的内容是否一致
+ * 内部会转换流为 {@link BufferedInputStream} + * + * @param input1 第一个reader + * @param input2 第二个reader + * @return 两个流的内容一致返回true,否则false + * @throws IORuntimeException IO异常 + * @since 4.0.6 + */ + public static boolean contentEquals(Reader input1, Reader input2) throws IORuntimeException { + input1 = getReader(input1); + input2 = getReader(input2); + + try { + int ch = input1.read(); + while (EOF != ch) { + int ch2 = input2.read(); + if (ch != ch2) { + return false; + } + ch = input1.read(); + } + + int ch2 = input2.read(); + return ch2 == EOF; + } catch (IOException e) { + throw new IORuntimeException(e); + } + } + + /** + * 对比两个流内容是否相同,忽略EOL字符
+ * 内部会转换流为 {@link BufferedInputStream} + * + * @param input1 第一个流 + * @param input2 第二个流 + * @return 两个流的内容一致返回true,否则false + * @throws IORuntimeException IO异常 + * @since 4.0.6 + */ + public static boolean contentEqualsIgnoreEOL(Reader input1, Reader input2) throws IORuntimeException { + final BufferedReader br1 = getReader(input1); + final BufferedReader br2 = getReader(input2); + + try { + String line1 = br1.readLine(); + String line2 = br2.readLine(); + while (line1 != null && line1.equals(line2)) { + line1 = br1.readLine(); + line2 = br2.readLine(); + } + return Objects.equals(line1, line2); + } catch (IOException e) { + throw new IORuntimeException(e); + } + } + + /** + * 计算流CRC32校验码,计算后关闭流 + * + * @param in 文件,不能为目录 + * @return CRC32值 + * @throws IORuntimeException IO异常 + * @since 4.0.6 + */ + public static long checksumCRC32(InputStream in) throws IORuntimeException { + return checksum(in, new CRC32()).getValue(); + } + + /** + * 计算流的校验码,计算后关闭流 + * + * @param in 流 + * @param checksum {@link Checksum} + * @return Checksum + * @throws IORuntimeException IO异常 + * @since 4.0.10 + */ + public static Checksum checksum(InputStream in, Checksum checksum) throws IORuntimeException { + Assert.notNull(in, "InputStream is null !"); + if (null == checksum) { + checksum = new CRC32(); + } + try { + in = new CheckedInputStream(in, checksum); + IoUtil.copy(in, new NullOutputStream()); + } finally { + IoUtil.close(in); + } + return checksum; + } + + /** + * 计算流的校验码,计算后关闭流 + * + * @param in 流 + * @param checksum {@link Checksum} + * @return Checksum + * @throws IORuntimeException IO异常 + * @since 5.4.0 + */ + public static long checksumValue(InputStream in, Checksum checksum) { + return checksum(in, checksum).getValue(); + } + + /** + * 返回行遍历器 + *
+	 * LineIterator it = null;
+	 * try {
+	 * 	it = IoUtil.lineIter(reader);
+	 * 	while (it.hasNext()) {
+	 * 		String line = it.nextLine();
+	 * 		// do something with line
+	 *    }
+	 * } finally {
+	 * 		it.close();
+	 * }
+	 * 
+ * + * @param reader {@link Reader} + * @return {@link LineIter} + * @since 5.6.1 + */ + public static LineIter lineIter(Reader reader) { + return new LineIter(reader); + } + + /** + * 返回行遍历器 + *
+	 * LineIterator it = null;
+	 * try {
+	 * 	it = IoUtil.lineIter(in, CharsetUtil.CHARSET_UTF_8);
+	 * 	while (it.hasNext()) {
+	 * 		String line = it.nextLine();
+	 * 		// do something with line
+	 *    }
+	 * } finally {
+	 * 		it.close();
+	 * }
+	 * 
+ * + * @param in {@link InputStream} + * @param charset 编码 + * @return {@link LineIter} + * @since 5.6.1 + */ + public static LineIter lineIter(InputStream in, Charset charset) { + return new LineIter(in, charset); + } + + /** + * {@link ByteArrayOutputStream} 转换为String + * @param out {@link ByteArrayOutputStream} + * @param charset 编码 + * @return 字符串 + * @since 5.7.17 + */ + public static String toStr(ByteArrayOutputStream out, Charset charset){ + return out.toString(charset); + } +} diff --git a/src/main/java/cn/hutool/core/io/LimitedInputStream.java b/src/main/java/cn/hutool/core/io/LimitedInputStream.java new file mode 100644 index 0000000..dfb51a0 --- /dev/null +++ b/src/main/java/cn/hutool/core/io/LimitedInputStream.java @@ -0,0 +1,63 @@ +package cn.hutool.core.io; + +import java.io.FilterInputStream; +import java.io.IOException; +import java.io.InputStream; + +/** + * 限制读取最大长度的{@link FilterInputStream} 实现
+ * 来自:https://github.com/skylot/jadx/blob/master/jadx-plugins/jadx-plugins-api/src/main/java/jadx/api/plugins/utils/LimitedInputStream.java + * + * @author jadx + */ +public class LimitedInputStream extends FilterInputStream { + + private final long maxSize; + private long currentPos; + + /** + * 构造 + * @param in {@link InputStream} + * @param maxSize 限制最大读取量,单位byte + */ + public LimitedInputStream(InputStream in, long maxSize) { + super(in); + this.maxSize = maxSize; + } + + @Override + public int read() throws IOException { + final int data = super.read(); + if (data != -1) { + currentPos++; + checkPos(); + } + return data; + } + + @Override + public int read(byte[] b, int off, int len) throws IOException { + final int count = super.read(b, off, len); + if (count > 0) { + currentPos += count; + checkPos(); + } + return count; + } + + @Override + public long skip(long n) throws IOException { + final long skipped = super.skip(n); + if (skipped != 0) { + currentPos += skipped; + checkPos(); + } + return skipped; + } + + private void checkPos() { + if (currentPos > maxSize) { + throw new IllegalStateException("Read limit exceeded"); + } + } +} diff --git a/src/main/java/cn/hutool/core/io/LineHandler.java b/src/main/java/cn/hutool/core/io/LineHandler.java new file mode 100644 index 0000000..57bec50 --- /dev/null +++ b/src/main/java/cn/hutool/core/io/LineHandler.java @@ -0,0 +1,15 @@ +package cn.hutool.core.io; + +/** + * 行处理器 + * @author Looly + * + */ +@FunctionalInterface +public interface LineHandler { + /** + * 处理一行数据,可以编辑后存入指定地方 + * @param line 行 + */ + void handle(String line); +} diff --git a/src/main/java/cn/hutool/core/io/ManifestUtil.java b/src/main/java/cn/hutool/core/io/ManifestUtil.java new file mode 100644 index 0000000..6cb985f --- /dev/null +++ b/src/main/java/cn/hutool/core/io/ManifestUtil.java @@ -0,0 +1,120 @@ +package cn.hutool.core.io; + +import cn.hutool.core.io.resource.ResourceUtil; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.net.JarURLConnection; +import java.net.URL; +import java.net.URLConnection; +import java.util.jar.JarFile; +import java.util.jar.Manifest; + +/** + * Jar包中manifest.mf文件获取和解析工具类 + * 来自Jodd + * + * @author looly, jodd + * @since 5.7.0 + */ +public class ManifestUtil { + private static final String[] MANIFEST_NAMES = {"Manifest.mf", "manifest.mf", "MANIFEST.MF"}; + + /** + * 根据 class 获取 所在 jar 包文件的 Manifest
+ * 此方法主要利用class定位jar包,如引入hutool-all,则传入hutool中任意一个类即可获取这个jar的Manifest信息
+ * 如果这个类不在jar包中,返回{@code null} + * + * @param cls 类 + * @return Manifest + * @throws IORuntimeException IO异常 + */ + public static Manifest getManifest(Class cls) throws IORuntimeException { + URL url = ResourceUtil.getResource(null, cls); + URLConnection connection; + try { + connection = url.openConnection(); + }catch (final IOException e) { + throw new IORuntimeException(e); + } + + if (connection instanceof JarURLConnection) { + JarURLConnection conn = (JarURLConnection) connection; + return getManifest(conn); + } + return null; + } + + /** + * 获取 jar 包文件或项目目录下的 Manifest + * + * @param classpathItem 文件路径 + * @return Manifest + * @throws IORuntimeException IO异常 + */ + public static Manifest getManifest(File classpathItem) throws IORuntimeException{ + Manifest manifest = null; + + if (classpathItem.isFile()) { + try (JarFile jarFile = new JarFile(classpathItem)){ + manifest = getManifest(jarFile); + } catch (final IOException e) { + throw new IORuntimeException(e); + } + } else { + final File metaDir = new File(classpathItem, "META-INF"); + File manifestFile = null; + if (metaDir.isDirectory()) { + for (final String name : MANIFEST_NAMES) { + final File mFile = new File(metaDir, name); + if (mFile.isFile()) { + manifestFile = mFile; + break; + } + } + } + if (null != manifestFile) { + try(FileInputStream fis = new FileInputStream(manifestFile)){ + manifest = new Manifest(fis); + } catch (final IOException e) { + throw new IORuntimeException(e); + } + } + } + + return manifest; + } + + /** + * 根据 {@link JarURLConnection} 获取 jar 包文件的 Manifest + * + * @param connection {@link JarURLConnection} + * @return Manifest + * @throws IORuntimeException IO异常 + */ + public static Manifest getManifest(JarURLConnection connection) throws IORuntimeException{ + final JarFile jarFile; + try { + jarFile = connection.getJarFile(); + } catch (IOException e) { + throw new IORuntimeException(e); + } + return getManifest(jarFile); + } + + /** + * 根据 {@link JarURLConnection} 获取 jar 包文件的 Manifest + * + * @param jarFile {@link JarURLConnection} + * @return Manifest + * @throws IORuntimeException IO异常 + */ + public static Manifest getManifest(JarFile jarFile) throws IORuntimeException { + try { + return jarFile.getManifest(); + } catch (IOException e) { + throw new IORuntimeException(e); + } + } +} diff --git a/src/main/java/cn/hutool/core/io/NioUtil.java b/src/main/java/cn/hutool/core/io/NioUtil.java new file mode 100644 index 0000000..215fd36 --- /dev/null +++ b/src/main/java/cn/hutool/core/io/NioUtil.java @@ -0,0 +1,275 @@ +package cn.hutool.core.io; + +import cn.hutool.core.io.copy.ChannelCopier; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.CharsetUtil; +import cn.hutool.core.util.StrUtil; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.MappedByteBuffer; +import java.nio.channels.Channels; +import java.nio.channels.FileChannel; +import java.nio.channels.ReadableByteChannel; +import java.nio.channels.WritableByteChannel; +import java.nio.charset.Charset; + +/** + * NIO相关工具封装,主要针对Channel读写、拷贝等封装 + * + * @author looly + * @since 5.5.3 + */ +public class NioUtil { + + /** + * 默认缓存大小 8192 + */ + public static final int DEFAULT_BUFFER_SIZE = 2 << 12; + /** + * 默认中等缓存大小 16384 + */ + public static final int DEFAULT_MIDDLE_BUFFER_SIZE = 2 << 13; + /** + * 默认大缓存大小 32768 + */ + public static final int DEFAULT_LARGE_BUFFER_SIZE = 2 << 14; + + /** + * 数据流末尾 + */ + public static final int EOF = -1; + + /** + * 拷贝流 thanks to: https://github.com/venusdrogon/feilong-io/blob/master/src/main/java/com/feilong/io/IOWriteUtil.java
+ * 本方法不会关闭流 + * + * @param in 输入流 + * @param out 输出流 + * @param bufferSize 缓存大小 + * @param streamProgress 进度条 + * @return 传输的byte数 + * @throws IORuntimeException IO异常 + */ + public static long copyByNIO(InputStream in, OutputStream out, int bufferSize, StreamProgress streamProgress) throws IORuntimeException { + return copyByNIO(in, out, bufferSize, -1, streamProgress); + } + + /** + * 拷贝流
+ * 本方法不会关闭流 + * + * @param in 输入流 + * @param out 输出流 + * @param bufferSize 缓存大小 + * @param count 最大长度 + * @param streamProgress 进度条 + * @return 传输的byte数 + * @throws IORuntimeException IO异常 + * @since 5.7.8 + */ + public static long copyByNIO(InputStream in, OutputStream out, int bufferSize, long count, StreamProgress streamProgress) throws IORuntimeException { + final long copySize = copy(Channels.newChannel(in), Channels.newChannel(out), bufferSize, count, streamProgress); + IoUtil.flush(out); + return copySize; + } + + /** + * 拷贝文件Channel,使用NIO,拷贝后不会关闭channel + * + * @param inChannel {@link FileChannel} + * @param outChannel {@link FileChannel} + * @return 拷贝的字节数 + * @throws IORuntimeException IO异常 + * @since 5.5.3 + */ + public static long copy(FileChannel inChannel, FileChannel outChannel) throws IORuntimeException { + Assert.notNull(inChannel, "In channel is null!"); + Assert.notNull(outChannel, "Out channel is null!"); + + try { + return copySafely(inChannel, outChannel); + } catch (IOException e) { + throw new IORuntimeException(e); + } + } + + /** + * 文件拷贝实现 + * + *
+	 * FileChannel#transferTo 或 FileChannel#transferFrom 的实现是平台相关的,需要确保低版本平台的兼容性
+	 * 例如 android 7以下平台在使用 ZipInputStream 解压文件的过程中,
+	 * 通过 FileChannel#transferFrom 传输到文件时,其返回值可能小于 totalBytes,不处理将导致文件内容缺失
+	 *
+	 * // 错误写法,dstChannel.transferFrom 返回值小于 zipEntry.getSize(),导致解压后文件内容缺失
+	 * try (InputStream srcStream = zipFile.getInputStream(zipEntry);
+	 * 		ReadableByteChannel srcChannel = Channels.newChannel(srcStream);
+	 * 		FileOutputStream fos = new FileOutputStream(saveFile);
+	 * 		FileChannel dstChannel = fos.getChannel()) {
+	 * 		dstChannel.transferFrom(srcChannel, 0, zipEntry.getSize());
+	 *  }
+	 * 
+ * + * @param inChannel 输入通道 + * @param outChannel 输出通道 + * @return 输入通道的字节数 + * @throws IOException 发生IO错误 + * @link http://androidxref.com/6.0.1_r10/xref/libcore/luni/src/main/java/java/nio/FileChannelImpl.java + * @link http://androidxref.com/7.0.0_r1/xref/libcore/ojluni/src/main/java/sun/nio/ch/FileChannelImpl.java + * @link http://androidxref.com/7.0.0_r1/xref/libcore/ojluni/src/main/native/FileChannelImpl.c + * @author z8g + * @since 5.7.21 + */ + private static long copySafely(FileChannel inChannel, FileChannel outChannel) throws IOException { + final long totalBytes = inChannel.size(); + for (long pos = 0, remaining = totalBytes; remaining > 0; ) { // 确保文件内容不会缺失 + final long writeBytes = inChannel.transferTo(pos, remaining, outChannel); // 实际传输的字节数 + pos += writeBytes; + remaining -= writeBytes; + } + return totalBytes; + } + + /** + * 拷贝流,使用NIO,不会关闭channel + * + * @param in {@link ReadableByteChannel} + * @param out {@link WritableByteChannel} + * @return 拷贝的字节数 + * @throws IORuntimeException IO异常 + * @since 4.5.0 + */ + public static long copy(ReadableByteChannel in, WritableByteChannel out) throws IORuntimeException { + return copy(in, out, DEFAULT_BUFFER_SIZE); + } + + /** + * 拷贝流,使用NIO,不会关闭channel + * + * @param in {@link ReadableByteChannel} + * @param out {@link WritableByteChannel} + * @param bufferSize 缓冲大小,如果小于等于0,使用默认 + * @return 拷贝的字节数 + * @throws IORuntimeException IO异常 + * @since 4.5.0 + */ + public static long copy(ReadableByteChannel in, WritableByteChannel out, int bufferSize) throws IORuntimeException { + return copy(in, out, bufferSize, null); + } + + /** + * 拷贝流,使用NIO,不会关闭channel + * + * @param in {@link ReadableByteChannel} + * @param out {@link WritableByteChannel} + * @param bufferSize 缓冲大小,如果小于等于0,使用默认 + * @param streamProgress {@link StreamProgress}进度处理器 + * @return 拷贝的字节数 + * @throws IORuntimeException IO异常 + */ + public static long copy(ReadableByteChannel in, WritableByteChannel out, int bufferSize, StreamProgress streamProgress) throws IORuntimeException { + return copy(in, out, bufferSize, -1, streamProgress); + } + + /** + * 拷贝流,使用NIO,不会关闭channel + * + * @param in {@link ReadableByteChannel} + * @param out {@link WritableByteChannel} + * @param bufferSize 缓冲大小,如果小于等于0,使用默认 + * @param count 读取总长度 + * @param streamProgress {@link StreamProgress}进度处理器 + * @return 拷贝的字节数 + * @throws IORuntimeException IO异常 + * @since 5.7.8 + */ + public static long copy(ReadableByteChannel in, WritableByteChannel out, int bufferSize, long count, StreamProgress streamProgress) throws IORuntimeException { + return new ChannelCopier(bufferSize, count, streamProgress).copy(in, out); + } + + /** + * 从流中读取内容,读取完毕后并不关闭流 + * + * @param channel 可读通道,读取完毕后并不关闭通道 + * @param charset 字符集 + * @return 内容 + * @throws IORuntimeException IO异常 + * @since 4.5.0 + */ + public static String read(ReadableByteChannel channel, Charset charset) throws IORuntimeException { + FastByteArrayOutputStream out = read(channel); + return null == charset ? out.toString() : out.toString(charset); + } + + /** + * 从流中读取内容,读到输出流中 + * + * @param channel 可读通道,读取完毕后并不关闭通道 + * @return 输出流 + * @throws IORuntimeException IO异常 + */ + public static FastByteArrayOutputStream read(ReadableByteChannel channel) throws IORuntimeException { + final FastByteArrayOutputStream out = new FastByteArrayOutputStream(); + copy(channel, Channels.newChannel(out)); + return out; + } + + /** + * 从FileChannel中读取UTF-8编码内容 + * + * @param fileChannel 文件管道 + * @return 内容 + * @throws IORuntimeException IO异常 + */ + public static String readUtf8(FileChannel fileChannel) throws IORuntimeException { + return read(fileChannel, CharsetUtil.CHARSET_UTF_8); + } + + /** + * 从FileChannel中读取内容,读取完毕后并不关闭Channel + * + * @param fileChannel 文件管道 + * @param charsetName 字符集 + * @return 内容 + * @throws IORuntimeException IO异常 + */ + public static String read(FileChannel fileChannel, String charsetName) throws IORuntimeException { + return read(fileChannel, CharsetUtil.charset(charsetName)); + } + + /** + * 从FileChannel中读取内容 + * + * @param fileChannel 文件管道 + * @param charset 字符集 + * @return 内容 + * @throws IORuntimeException IO异常 + */ + public static String read(FileChannel fileChannel, Charset charset) throws IORuntimeException { + MappedByteBuffer buffer; + try { + buffer = fileChannel.map(FileChannel.MapMode.READ_ONLY, 0, fileChannel.size()).load(); + } catch (IOException e) { + throw new IORuntimeException(e); + } + return StrUtil.str(buffer, charset); + } + + /** + * 关闭
+ * 关闭失败不会抛出异常 + * + * @param closeable 被关闭的对象 + */ + public static void close(AutoCloseable closeable) { + if (null != closeable) { + try { + closeable.close(); + } catch (Exception e) { + // 静默关闭 + } + } + } +} diff --git a/src/main/java/cn/hutool/core/io/NullOutputStream.java b/src/main/java/cn/hutool/core/io/NullOutputStream.java new file mode 100644 index 0000000..f25ef87 --- /dev/null +++ b/src/main/java/cn/hutool/core/io/NullOutputStream.java @@ -0,0 +1,53 @@ +package cn.hutool.core.io; + +import java.io.IOException; +import java.io.OutputStream; + +/** + * 此OutputStream写出数据到/dev/null,即忽略所有数据
+ * 来自 Apache Commons io + * + * @author looly + * @since 4.0.6 + */ +public class NullOutputStream extends OutputStream { + + /** + * 单例 + */ + public static final NullOutputStream NULL_OUTPUT_STREAM = new NullOutputStream(); + + /** + * 什么也不做,写出到/dev/null. + * + * @param b 写出的数据 + * @param off 开始位置 + * @param len 长度 + */ + @Override + public void write(byte[] b, int off, int len) { + // to /dev/null + } + + /** + * 什么也不做,写出到 /dev/null. + * + * @param b 写出的数据 + */ + @Override + public void write(int b) { + // to /dev/null + } + + /** + * 什么也不做,写出到 /dev/null. + * + * @param b 写出的数据 + * @throws IOException 不抛出 + */ + @Override + public void write(byte[] b) throws IOException { + // to /dev/null + } + +} diff --git a/src/main/java/cn/hutool/core/io/StreamProgress.java b/src/main/java/cn/hutool/core/io/StreamProgress.java new file mode 100644 index 0000000..fb72d70 --- /dev/null +++ b/src/main/java/cn/hutool/core/io/StreamProgress.java @@ -0,0 +1,29 @@ +package cn.hutool.core.io; + +/** + * Stream进度条
+ * 提供流拷贝进度监测,如开始、结束触发,以及进度回调。
+ * 注意进度回调的{@code total}参数为总大小,某些场景下无总大小的标记,则此值应为-1或者{@link Long#MAX_VALUE},表示此参数无效。 + * + * @author Looly + */ +public interface StreamProgress { + + /** + * 开始 + */ + void start(); + + /** + * 进行中 + * + * @param total 总大小,如果未知为 -1或者{@link Long#MAX_VALUE} + * @param progressSize 已经进行的大小 + */ + void progress(long total, long progressSize); + + /** + * 结束 + */ + void finish(); +} diff --git a/src/main/java/cn/hutool/core/io/ValidateObjectInputStream.java b/src/main/java/cn/hutool/core/io/ValidateObjectInputStream.java new file mode 100644 index 0000000..e643895 --- /dev/null +++ b/src/main/java/cn/hutool/core/io/ValidateObjectInputStream.java @@ -0,0 +1,101 @@ +package cn.hutool.core.io; + +import cn.hutool.core.collection.CollUtil; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InvalidClassException; +import java.io.ObjectInputStream; +import java.io.ObjectStreamClass; +import java.util.HashSet; +import java.util.Set; + +/** + * 带有类验证的对象流,用于避免反序列化漏洞
+ * 详细见:https://xz.aliyun.com/t/41/ + * + * @author looly + * @since 5.2.6 + */ +public class ValidateObjectInputStream extends ObjectInputStream { + + private Set whiteClassSet; + private Set blackClassSet; + + /** + * 构造 + * + * @param inputStream 流 + * @param acceptClasses 白名单的类 + * @throws IOException IO异常 + */ + public ValidateObjectInputStream(InputStream inputStream, Class... acceptClasses) throws IOException { + super(inputStream); + accept(acceptClasses); + } + + /** + * 禁止反序列化的类,用于反序列化验证 + * + * @param refuseClasses 禁止反序列化的类 + * @since 5.3.5 + */ + public void refuse(Class... refuseClasses) { + if(null == this.blackClassSet){ + this.blackClassSet = new HashSet<>(); + } + for (Class acceptClass : refuseClasses) { + this.blackClassSet.add(acceptClass.getName()); + } + } + + /** + * 接受反序列化的类,用于反序列化验证 + * + * @param acceptClasses 接受反序列化的类 + */ + public void accept(Class... acceptClasses) { + if(null == this.whiteClassSet){ + this.whiteClassSet = new HashSet<>(); + } + for (Class acceptClass : acceptClasses) { + this.whiteClassSet.add(acceptClass.getName()); + } + } + + /** + * 只允许反序列化SerialObject class + */ + @Override + protected Class resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException { + validateClassName(desc.getName()); + return super.resolveClass(desc); + } + + /** + * 验证反序列化的类是否合法 + * @param className 类名 + * @throws InvalidClassException 非法类 + */ + private void validateClassName(String className) throws InvalidClassException { + // 黑名单 + if(CollUtil.isNotEmpty(this.blackClassSet)){ + if(this.blackClassSet.contains(className)){ + throw new InvalidClassException("Unauthorized deserialization attempt by black list", className); + } + } + + if(CollUtil.isEmpty(this.whiteClassSet)){ + return; + } + if(className.startsWith("java.")){ + // java中的类默认在白名单中 + return; + } + if(this.whiteClassSet.contains(className)){ + return; + } + + throw new InvalidClassException("Unauthorized deserialization attempt", className); + } +} diff --git a/src/main/java/cn/hutool/core/io/checksum/CRC16.java b/src/main/java/cn/hutool/core/io/checksum/CRC16.java new file mode 100644 index 0000000..26126a6 --- /dev/null +++ b/src/main/java/cn/hutool/core/io/checksum/CRC16.java @@ -0,0 +1,73 @@ +package cn.hutool.core.io.checksum; + +import cn.hutool.core.io.checksum.crc16.CRC16Checksum; +import cn.hutool.core.io.checksum.crc16.CRC16IBM; + +import java.io.Serializable; +import java.util.zip.Checksum; + +/** + * CRC16 循环冗余校验码(Cyclic Redundancy Check)实现,默认IBM算法 + * + * @author looly + * @since 4.4.1 + */ +public class CRC16 implements Checksum, Serializable { + private static final long serialVersionUID = 1L; + + private final CRC16Checksum crc16; + + public CRC16() { + this(new CRC16IBM()); + } + + /** + * 构造 + * + * @param crc16Checksum {@link CRC16Checksum} 实现 + */ + public CRC16(CRC16Checksum crc16Checksum) { + this.crc16 = crc16Checksum; + } + + /** + * 获取16进制的CRC16值 + * + * @return 16进制的CRC16值 + * @since 5.7.22 + */ + public String getHexValue() { + return this.crc16.getHexValue(); + } + + /** + * 获取16进制的CRC16值 + * + * @param isPadding 不足4位时,是否填充0以满足位数 + * @return 16进制的CRC16值,4位 + * @since 5.7.22 + */ + public String getHexValue(boolean isPadding) { + return crc16.getHexValue(isPadding); + } + + @Override + public long getValue() { + return crc16.getValue(); + } + + @Override + public void reset() { + crc16.reset(); + } + + @Override + public void update(byte[] b, int off, int len) { + crc16.update(b, off, len); + } + + @Override + public void update(int b) { + crc16.update(b); + } +} diff --git a/src/main/java/cn/hutool/core/io/checksum/CRC8.java b/src/main/java/cn/hutool/core/io/checksum/CRC8.java new file mode 100644 index 0000000..e9bd387 --- /dev/null +++ b/src/main/java/cn/hutool/core/io/checksum/CRC8.java @@ -0,0 +1,72 @@ +package cn.hutool.core.io.checksum; + +import java.io.Serializable; +import java.util.zip.Checksum; + +/** + * CRC8 循环冗余校验码(Cyclic Redundancy Check)实现
+ * 代码来自:https://github.com/BBSc0der + * + * @author Bolek,Looly + * @since 4.4.1 + */ +public class CRC8 implements Checksum, Serializable { + private static final long serialVersionUID = 1L; + + private final short init; + private final short[] crcTable = new short[256]; + private short value; + + /** + * 构造
+ * + * @param polynomial Polynomial, typically one of the POLYNOMIAL_* constants. + * @param init Initial value, typically either 0xff or zero. + */ + public CRC8(int polynomial, short init) { + this.value = this.init = init; + for (int dividend = 0; dividend < 256; dividend++) { + int remainder = dividend;// << 8; + for (int bit = 0; bit < 8; ++bit) { + if ((remainder & 0x01) != 0) { + remainder = (remainder >>> 1) ^ polynomial; + } else { + remainder >>>= 1; + } + } + crcTable[dividend] = (short) remainder; + } + } + + @Override + public void update(byte[] buffer, int offset, int len) { + for (int i = 0; i < len; i++) { + int data = buffer[offset + i] ^ value; + value = (short) (crcTable[data & 0xff] ^ (value << 8)); + } + } + + /** + * Updates the current checksum with the specified array of bytes. Equivalent to calling update(buffer, 0, buffer.length). + * + * @param buffer the byte array to update the checksum with + */ + public void update(byte[] buffer) { + update(buffer, 0, buffer.length); + } + + @Override + public void update(int b) { + update(new byte[] { (byte) b }, 0, 1); + } + + @Override + public long getValue() { + return value & 0xff; + } + + @Override + public void reset() { + value = init; + } +} diff --git a/src/main/java/cn/hutool/core/io/checksum/crc16/CRC16Ansi.java b/src/main/java/cn/hutool/core/io/checksum/crc16/CRC16Ansi.java new file mode 100644 index 0000000..c43a14a --- /dev/null +++ b/src/main/java/cn/hutool/core/io/checksum/crc16/CRC16Ansi.java @@ -0,0 +1,33 @@ +package cn.hutool.core.io.checksum.crc16; + +/** + * CRC16_ANSI + * + * @author looly + * @since 5.3.10 + */ +public class CRC16Ansi extends CRC16Checksum{ + private static final long serialVersionUID = 1L; + + private static final int WC_POLY = 0xa001; + + @Override + public void reset() { + this.wCRCin = 0xffff; + } + + @Override + public void update(int b) { + int hi = wCRCin >> 8; + hi ^= b; + wCRCin = hi; + + for (int i = 0; i < 8; i++) { + int flag = wCRCin & 0x0001; + wCRCin = wCRCin >> 1; + if (flag == 1) { + wCRCin ^= WC_POLY; + } + } + } +} diff --git a/src/main/java/cn/hutool/core/io/checksum/crc16/CRC16CCITT.java b/src/main/java/cn/hutool/core/io/checksum/crc16/CRC16CCITT.java new file mode 100644 index 0000000..13e7dd9 --- /dev/null +++ b/src/main/java/cn/hutool/core/io/checksum/crc16/CRC16CCITT.java @@ -0,0 +1,27 @@ +package cn.hutool.core.io.checksum.crc16; + +/** + * CRC16_CCITT:多项式x16+x12+x5+1(0x1021),初始值0x0000,低位在前,高位在后,结果与0x0000异或 + * 0x8408是0x1021按位颠倒后的结果。 + * + * @author looly + * @since 5.3.10 + */ +public class CRC16CCITT extends CRC16Checksum{ + private static final long serialVersionUID = 1L; + + private static final int WC_POLY = 0x8408; + + @Override + public void update(int b) { + wCRCin ^= (b & 0x00ff); + for (int j = 0; j < 8; j++) { + if ((wCRCin & 0x0001) != 0) { + wCRCin >>= 1; + wCRCin ^= WC_POLY; + } else { + wCRCin >>= 1; + } + } + } +} diff --git a/src/main/java/cn/hutool/core/io/checksum/crc16/CRC16CCITTFalse.java b/src/main/java/cn/hutool/core/io/checksum/crc16/CRC16CCITTFalse.java new file mode 100644 index 0000000..3a8ef42 --- /dev/null +++ b/src/main/java/cn/hutool/core/io/checksum/crc16/CRC16CCITTFalse.java @@ -0,0 +1,35 @@ +package cn.hutool.core.io.checksum.crc16; + +/** + * CRC16_CCITT_FALSE:多项式x16+x12+x5+1(0x1021),初始值0xFFFF,低位在后,高位在前,结果与0x0000异或 + * + * @author looly + * @since 5.3.10 + */ +public class CRC16CCITTFalse extends CRC16Checksum{ + private static final long serialVersionUID = 1L; + + private static final int WC_POLY = 0x1021; + + @Override + public void reset() { + this.wCRCin = 0xffff; + } + + @Override + public void update(byte[] b, int off, int len) { + super.update(b, off, len); + wCRCin &= 0xffff; + } + + @Override + public void update(int b) { + for (int i = 0; i < 8; i++) { + boolean bit = ((b >> (7 - i) & 1) == 1); + boolean c15 = ((wCRCin >> 15 & 1) == 1); + wCRCin <<= 1; + if (c15 ^ bit) + wCRCin ^= WC_POLY; + } + } +} diff --git a/src/main/java/cn/hutool/core/io/checksum/crc16/CRC16Checksum.java b/src/main/java/cn/hutool/core/io/checksum/crc16/CRC16Checksum.java new file mode 100644 index 0000000..511dccf --- /dev/null +++ b/src/main/java/cn/hutool/core/io/checksum/crc16/CRC16Checksum.java @@ -0,0 +1,75 @@ +package cn.hutool.core.io.checksum.crc16; + +import cn.hutool.core.util.HexUtil; +import cn.hutool.core.util.StrUtil; + +import java.io.Serializable; +import java.util.zip.Checksum; + +/** + * CRC16 Checksum,用于提供多种CRC16算法的通用实现
+ * 通过继承此类,重写update和reset完成相应算法。 + * + * @author looly + * @since 5.3.10 + */ +public abstract class CRC16Checksum implements Checksum, Serializable { + private static final long serialVersionUID = 1L; + + /** + * CRC16 Checksum 结果值 + */ + protected int wCRCin; + + public CRC16Checksum(){ + reset(); + } + + @Override + public long getValue() { + return wCRCin; + } + + /** + * 获取16进制的CRC16值 + * + * @return 16进制的CRC16值 + */ + public String getHexValue(){ + return getHexValue(false); + } + + /** + * 获取16进制的CRC16值 + * @param isPadding 不足4位时,是否填充0以满足位数 + * @return 16进制的CRC16值,4位 + */ + public String getHexValue(boolean isPadding){ + String hex = HexUtil.toHex(getValue()); + if(isPadding){ + hex = StrUtil.padPre(hex, 4, '0'); + } + + return hex; + } + + @Override + public void reset() { + wCRCin = 0x0000; + } + + /** + * 计算全部字节 + * @param b 字节 + */ + public void update(byte[] b){ + update(b, 0, b.length); + } + + @Override + public void update(byte[] b, int off, int len) { + for (int i = off; i < off + len; i++) + update(b[i]); + } + +} diff --git a/src/main/java/cn/hutool/core/io/checksum/crc16/CRC16DNP.java b/src/main/java/cn/hutool/core/io/checksum/crc16/CRC16DNP.java new file mode 100644 index 0000000..87e4369 --- /dev/null +++ b/src/main/java/cn/hutool/core/io/checksum/crc16/CRC16DNP.java @@ -0,0 +1,33 @@ +package cn.hutool.core.io.checksum.crc16; + +/** + * CRC16_DNP:多项式x16+x13+x12+x11+x10+x8+x6+x5+x2+1(0x3D65),初始值0x0000,低位在前,高位在后,结果与0xFFFF异或 + * 0xA6BC是0x3D65按位颠倒后的结果 + * + * @author looly + * @since 5.3.10 + */ +public class CRC16DNP extends CRC16Checksum{ + private static final long serialVersionUID = 1L; + + private static final int WC_POLY = 0xA6BC; + + @Override + public void update(byte[] b, int off, int len) { + super.update(b, off, len); + wCRCin ^= 0xffff; + } + + @Override + public void update(int b) { + wCRCin ^= (b & 0x00ff); + for (int j = 0; j < 8; j++) { + if ((wCRCin & 0x0001) != 0) { + wCRCin >>= 1; + wCRCin ^= WC_POLY; + } else { + wCRCin >>= 1; + } + } + } +} diff --git a/src/main/java/cn/hutool/core/io/checksum/crc16/CRC16IBM.java b/src/main/java/cn/hutool/core/io/checksum/crc16/CRC16IBM.java new file mode 100644 index 0000000..4b8a7dc --- /dev/null +++ b/src/main/java/cn/hutool/core/io/checksum/crc16/CRC16IBM.java @@ -0,0 +1,27 @@ +package cn.hutool.core.io.checksum.crc16; + +/** + * CRC16_IBM:多项式x16+x15+x2+1(0x8005),初始值0x0000,低位在前,高位在后,结果与0x0000异或 + * 0xA001是0x8005按位颠倒后的结果 + * + * @author looly + * @since 5.3.10 + */ +public class CRC16IBM extends CRC16Checksum{ + private static final long serialVersionUID = 1L; + + private static final int WC_POLY = 0xa001; + + @Override + public void update(int b) { + wCRCin ^= (b & 0x00ff); + for (int j = 0; j < 8; j++) { + if ((wCRCin & 0x0001) != 0) { + wCRCin >>= 1; + wCRCin ^= WC_POLY; + } else { + wCRCin >>= 1; + } + } + } +} diff --git a/src/main/java/cn/hutool/core/io/checksum/crc16/CRC16Maxim.java b/src/main/java/cn/hutool/core/io/checksum/crc16/CRC16Maxim.java new file mode 100644 index 0000000..325f081 --- /dev/null +++ b/src/main/java/cn/hutool/core/io/checksum/crc16/CRC16Maxim.java @@ -0,0 +1,33 @@ +package cn.hutool.core.io.checksum.crc16; + +/** + * CRC16_MAXIM:多项式x16+x15+x2+1(0x8005),初始值0x0000,低位在前,高位在后,结果与0xFFFF异或 + * 0xA001是0x8005按位颠倒后的结果 + * + * @author looly + * @since 5.3.10 + */ +public class CRC16Maxim extends CRC16Checksum{ + private static final long serialVersionUID = 1L; + + private static final int WC_POLY = 0xa001; + + @Override + public void update(byte[] b, int off, int len) { + super.update(b, off, len); + wCRCin ^= 0xffff; + } + + @Override + public void update(int b) { + wCRCin ^= (b & 0x00ff); + for (int j = 0; j < 8; j++) { + if ((wCRCin & 0x0001) != 0) { + wCRCin >>= 1; + wCRCin ^= WC_POLY; + } else { + wCRCin >>= 1; + } + } + } +} diff --git a/src/main/java/cn/hutool/core/io/checksum/crc16/CRC16Modbus.java b/src/main/java/cn/hutool/core/io/checksum/crc16/CRC16Modbus.java new file mode 100644 index 0000000..538e24b --- /dev/null +++ b/src/main/java/cn/hutool/core/io/checksum/crc16/CRC16Modbus.java @@ -0,0 +1,33 @@ +package cn.hutool.core.io.checksum.crc16; + +/** + * CRC-16 (Modbus) + * CRC16_MODBUS:多项式x16+x15+x2+1(0x8005),初始值0xFFFF,低位在前,高位在后,结果与0x0000异或 + * 0xA001是0x8005按位颠倒后的结果 + * + * @author looly + * @since 5.3.10 + */ +public class CRC16Modbus extends CRC16Checksum{ + private static final long serialVersionUID = 1L; + + private static final int WC_POLY = 0xa001; + + @Override + public void reset(){ + this.wCRCin = 0xffff; + } + + @Override + public void update(int b) { + wCRCin ^= (b & 0x00ff); + for (int j = 0; j < 8; j++) { + if ((wCRCin & 0x0001) != 0) { + wCRCin >>= 1; + wCRCin ^= WC_POLY; + } else { + wCRCin >>= 1; + } + } + } +} diff --git a/src/main/java/cn/hutool/core/io/checksum/crc16/CRC16USB.java b/src/main/java/cn/hutool/core/io/checksum/crc16/CRC16USB.java new file mode 100644 index 0000000..7f0ce38 --- /dev/null +++ b/src/main/java/cn/hutool/core/io/checksum/crc16/CRC16USB.java @@ -0,0 +1,38 @@ +package cn.hutool.core.io.checksum.crc16; + +/** + * CRC16_USB:多项式x16+x15+x2+1(0x8005),初始值0xFFFF,低位在前,高位在后,结果与0xFFFF异或 + * 0xA001是0x8005按位颠倒后的结果 + * + * @author looly + * @since 5.3.10 + */ +public class CRC16USB extends CRC16Checksum{ + private static final long serialVersionUID = 1L; + + private static final int WC_POLY = 0xa001; + + @Override + public void reset(){ + this.wCRCin = 0xFFFF; + } + + @Override + public void update(byte[] b, int off, int len) { + super.update(b, off, len); + wCRCin ^= 0xffff; + } + + @Override + public void update(int b) { + wCRCin ^= (b & 0x00ff); + for (int j = 0; j < 8; j++) { + if ((wCRCin & 0x0001) != 0) { + wCRCin >>= 1; + wCRCin ^= WC_POLY; + } else { + wCRCin >>= 1; + } + } + } +} diff --git a/src/main/java/cn/hutool/core/io/checksum/crc16/CRC16X25.java b/src/main/java/cn/hutool/core/io/checksum/crc16/CRC16X25.java new file mode 100644 index 0000000..2e60de3 --- /dev/null +++ b/src/main/java/cn/hutool/core/io/checksum/crc16/CRC16X25.java @@ -0,0 +1,38 @@ +package cn.hutool.core.io.checksum.crc16; + +/** + * CRC16_X25:多项式x16+x12+x5+1(0x1021),初始值0xffff,低位在前,高位在后,结果与0xFFFF异或 + * 0x8408是0x1021按位颠倒后的结果。 + * + * @author looly + * @since 5.3.10 + */ +public class CRC16X25 extends CRC16Checksum{ + private static final long serialVersionUID = 1L; + + private static final int WC_POLY = 0x8408; + + @Override + public void reset(){ + this.wCRCin = 0xffff; + } + + @Override + public void update(byte[] b, int off, int len) { + super.update(b, off, len); + wCRCin ^= 0xffff; + } + + @Override + public void update(int b) { + wCRCin ^= (b & 0x00ff); + for (int j = 0; j < 8; j++) { + if ((wCRCin & 0x0001) != 0) { + wCRCin >>= 1; + wCRCin ^= WC_POLY; + } else { + wCRCin >>= 1; + } + } + } +} diff --git a/src/main/java/cn/hutool/core/io/checksum/crc16/CRC16XModem.java b/src/main/java/cn/hutool/core/io/checksum/crc16/CRC16XModem.java new file mode 100644 index 0000000..bc1e823 --- /dev/null +++ b/src/main/java/cn/hutool/core/io/checksum/crc16/CRC16XModem.java @@ -0,0 +1,32 @@ +package cn.hutool.core.io.checksum.crc16; + +/** + * CRC-CCITT (XModem) + * CRC16_XMODEM:多项式x16+x12+x5+1(0x1021),初始值0x0000,低位在后,高位在前,结果与0x0000异或 + * + * @author looly + * @since 5.3.10 + */ +public class CRC16XModem extends CRC16Checksum{ + private static final long serialVersionUID = 1L; + + // 0001 0000 0010 0001 (0, 5, 12) + private static final int WC_POLY = 0x1021; + + @Override + public void update(byte[] b, int off, int len) { + super.update(b, off, len); + wCRCin &= 0xffff; + } + + @Override + public void update(int b) { + for (int i = 0; i < 8; i++) { + boolean bit = ((b >> (7 - i) & 1) == 1); + boolean c15 = ((wCRCin >> 15 & 1) == 1); + wCRCin <<= 1; + if (c15 ^ bit) + wCRCin ^= WC_POLY; + } + } +} diff --git a/src/main/java/cn/hutool/core/io/checksum/crc16/package-info.java b/src/main/java/cn/hutool/core/io/checksum/crc16/package-info.java new file mode 100644 index 0000000..c881e7f --- /dev/null +++ b/src/main/java/cn/hutool/core/io/checksum/crc16/package-info.java @@ -0,0 +1,7 @@ +/** + * CRC16相关算法封装为Checksum + * + * @author looly + * + */ +package cn.hutool.core.io.checksum.crc16; \ No newline at end of file diff --git a/src/main/java/cn/hutool/core/io/checksum/package-info.java b/src/main/java/cn/hutool/core/io/checksum/package-info.java new file mode 100644 index 0000000..bc1ae41 --- /dev/null +++ b/src/main/java/cn/hutool/core/io/checksum/package-info.java @@ -0,0 +1,7 @@ +/** + * IO校验相关库和工具 + * + * @author looly + * + */ +package cn.hutool.core.io.checksum; \ No newline at end of file diff --git a/src/main/java/cn/hutool/core/io/copy/ChannelCopier.java b/src/main/java/cn/hutool/core/io/copy/ChannelCopier.java new file mode 100644 index 0000000..b453f47 --- /dev/null +++ b/src/main/java/cn/hutool/core/io/copy/ChannelCopier.java @@ -0,0 +1,116 @@ +package cn.hutool.core.io.copy; + +import cn.hutool.core.io.IORuntimeException; +import cn.hutool.core.io.IoUtil; +import cn.hutool.core.io.StreamProgress; +import cn.hutool.core.lang.Assert; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.ByteBuffer; +import java.nio.channels.ReadableByteChannel; +import java.nio.channels.WritableByteChannel; + +/** + * {@link ReadableByteChannel} 向 {@link WritableByteChannel} 拷贝 + * + * @author looly + * @since 5.7.8 + */ +public class ChannelCopier extends IoCopier { + + /** + * 构造 + */ + public ChannelCopier() { + this(IoUtil.DEFAULT_BUFFER_SIZE); + } + + /** + * 构造 + * + * @param bufferSize 缓存大小 + */ + public ChannelCopier(int bufferSize) { + this(bufferSize, -1); + } + + /** + * 构造 + * + * @param bufferSize 缓存大小 + * @param count 拷贝总数 + */ + public ChannelCopier(int bufferSize, long count) { + this(bufferSize, count, null); + } + + /** + * 构造 + * + * @param bufferSize 缓存大小 + * @param count 拷贝总数 + * @param progress 进度条 + */ + public ChannelCopier(int bufferSize, long count, StreamProgress progress) { + super(bufferSize, count, progress); + } + + @Override + public long copy(ReadableByteChannel source, WritableByteChannel target) { + Assert.notNull(source, "InputStream is null !"); + Assert.notNull(target, "OutputStream is null !"); + + final StreamProgress progress = this.progress; + if (null != progress) { + progress.start(); + } + final long size; + try { + size = doCopy(source, target, ByteBuffer.allocate(bufferSize(this.count)), progress); + } catch (IOException e) { + throw new IORuntimeException(e); + } + + if (null != progress) { + progress.finish(); + } + return size; + } + + /** + * 执行拷贝,如果限制最大长度,则按照最大长度读取,否则一直读取直到遇到-1 + * + * @param source {@link InputStream} + * @param target {@link OutputStream} + * @param buffer 缓存 + * @param progress 进度条 + * @return 拷贝总长度 + * @throws IOException IO异常 + */ + private long doCopy(ReadableByteChannel source, WritableByteChannel target, ByteBuffer buffer, StreamProgress progress) throws IOException { + long numToRead = this.count > 0 ? this.count : Long.MAX_VALUE; + long total = 0; + + int read; + while (numToRead > 0) { + read = source.read(buffer); + if (read < 0) { + // 提前读取到末尾 + break; + } + buffer.flip();// 写转读 + target.write(buffer); + buffer.clear(); + + numToRead -= read; + total += read; + if (null != progress) { + progress.progress(this.count, total); + } + } + + return total; + } +} diff --git a/src/main/java/cn/hutool/core/io/copy/IoCopier.java b/src/main/java/cn/hutool/core/io/copy/IoCopier.java new file mode 100644 index 0000000..156da58 --- /dev/null +++ b/src/main/java/cn/hutool/core/io/copy/IoCopier.java @@ -0,0 +1,76 @@ +package cn.hutool.core.io.copy; + +import cn.hutool.core.io.IoUtil; +import cn.hutool.core.io.StreamProgress; + +/** + * IO拷贝抽象,可自定义包括缓存、进度条等信息
+ * 此对象非线程安全 + * + * @param 拷贝源类型,如InputStream、Reader等 + * @param 拷贝目标类型,如OutputStream、Writer等 + * @author looly + * @since 5.7.8 + */ +public abstract class IoCopier { + + protected final int bufferSize; + /** + * 拷贝总数 + */ + protected final long count; + + /** + * 进度条 + */ + protected StreamProgress progress; + + /** + * 是否每次写出一个buffer内容就执行flush + */ + protected boolean flushEveryBuffer; + + /** + * 构造 + * + * @param bufferSize 缓存大小,< 0 表示默认{@link IoUtil#DEFAULT_BUFFER_SIZE} + * @param count 拷贝总数,-1表示无限制 + * @param progress 进度条 + */ + public IoCopier(int bufferSize, long count, StreamProgress progress) { + this.bufferSize = bufferSize > 0 ? bufferSize : IoUtil.DEFAULT_BUFFER_SIZE; + this.count = count <= 0 ? Long.MAX_VALUE : count; + this.progress = progress; + } + + /** + * 执行拷贝 + * + * @param source 拷贝源,如InputStream、Reader等 + * @param target 拷贝目标,如OutputStream、Writer等 + * @return 拷贝的实际长度 + */ + public abstract long copy(S source, T target); + + /** + * 缓存大小,取默认缓存和目标长度最小值 + * + * @param count 目标长度 + * @return 缓存大小 + */ + protected int bufferSize(long count) { + return (int) Math.min(this.bufferSize, count); + } + + /** + * 设置是否每次写出一个buffer内容就执行flush + * + * @param flushEveryBuffer 是否每次写出一个buffer内容就执行flush + * @return this + * @since 5.7.18 + */ + public IoCopier setFlushEveryBuffer(boolean flushEveryBuffer){ + this.flushEveryBuffer = flushEveryBuffer; + return this; + } +} diff --git a/src/main/java/cn/hutool/core/io/copy/ReaderWriterCopier.java b/src/main/java/cn/hutool/core/io/copy/ReaderWriterCopier.java new file mode 100644 index 0000000..9bec10d --- /dev/null +++ b/src/main/java/cn/hutool/core/io/copy/ReaderWriterCopier.java @@ -0,0 +1,117 @@ +package cn.hutool.core.io.copy; + +import cn.hutool.core.io.IORuntimeException; +import cn.hutool.core.io.IoUtil; +import cn.hutool.core.io.StreamProgress; +import cn.hutool.core.lang.Assert; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.Reader; +import java.io.Writer; + +/** + * {@link Reader} 向 {@link Writer} 拷贝 + * + * @author looly + * @since 5.7.8 + */ +public class ReaderWriterCopier extends IoCopier { + + /** + * 构造 + */ + public ReaderWriterCopier() { + this(IoUtil.DEFAULT_BUFFER_SIZE); + } + + /** + * 构造 + * + * @param bufferSize 缓存大小 + */ + public ReaderWriterCopier(int bufferSize) { + this(bufferSize, -1); + } + + /** + * 构造 + * + * @param bufferSize 缓存大小 + * @param count 拷贝总数 + */ + public ReaderWriterCopier(int bufferSize, long count) { + this(bufferSize, count, null); + } + + /** + * 构造 + * + * @param bufferSize 缓存大小 + * @param count 拷贝总数 + * @param progress 进度条 + */ + public ReaderWriterCopier(int bufferSize, long count, StreamProgress progress) { + super(bufferSize, count, progress); + } + + @Override + public long copy(Reader source, Writer target) { + Assert.notNull(source, "InputStream is null !"); + Assert.notNull(target, "OutputStream is null !"); + + final StreamProgress progress = this.progress; + if (null != progress) { + progress.start(); + } + final long size; + try { + size = doCopy(source, target, new char[bufferSize(this.count)], progress); + target.flush(); + } catch (IOException e) { + throw new IORuntimeException(e); + } + + if (null != progress) { + progress.finish(); + } + return size; + } + + /** + * 执行拷贝,如果限制最大长度,则按照最大长度读取,否则一直读取直到遇到-1 + * + * @param source {@link InputStream} + * @param target {@link OutputStream} + * @param buffer 缓存 + * @param progress 进度条 + * @return 拷贝总长度 + * @throws IOException IO异常 + */ + private long doCopy(Reader source, Writer target, char[] buffer, StreamProgress progress) throws IOException { + long numToRead = this.count > 0 ? this.count : Long.MAX_VALUE; + long total = 0; + + int read; + while (numToRead > 0) { + read = source.read(buffer, 0, bufferSize(numToRead)); + if (read < 0) { + // 提前读取到末尾 + break; + } + target.write(buffer, 0, read); + if(flushEveryBuffer){ + target.flush(); + } + + numToRead -= read; + total += read; + if (null != progress) { + progress.progress(this.count, total); + } + } + + return total; + } +} diff --git a/src/main/java/cn/hutool/core/io/copy/StreamCopier.java b/src/main/java/cn/hutool/core/io/copy/StreamCopier.java new file mode 100644 index 0000000..d3c7841 --- /dev/null +++ b/src/main/java/cn/hutool/core/io/copy/StreamCopier.java @@ -0,0 +1,116 @@ +package cn.hutool.core.io.copy; + +import cn.hutool.core.io.IORuntimeException; +import cn.hutool.core.io.IoUtil; +import cn.hutool.core.io.StreamProgress; +import cn.hutool.core.lang.Assert; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +/** + * {@link InputStream} 向 {@link OutputStream} 拷贝 + * + * @author looly + * @since 5.7.8 + */ +public class StreamCopier extends IoCopier { + + /** + * 构造 + */ + public StreamCopier() { + this(IoUtil.DEFAULT_BUFFER_SIZE); + } + + /** + * 构造 + * + * @param bufferSize 缓存大小 + */ + public StreamCopier(int bufferSize) { + this(bufferSize, -1); + } + + /** + * 构造 + * + * @param bufferSize 缓存大小 + * @param count 拷贝总数 + */ + public StreamCopier(int bufferSize, long count) { + this(bufferSize, count, null); + } + + /** + * 构造 + * + * @param bufferSize 缓存大小 + * @param count 拷贝总数 + * @param progress 进度条 + */ + public StreamCopier(int bufferSize, long count, StreamProgress progress) { + super(bufferSize, count, progress); + } + + @Override + public long copy(InputStream source, OutputStream target) { + Assert.notNull(source, "InputStream is null !"); + Assert.notNull(target, "OutputStream is null !"); + + final StreamProgress progress = this.progress; + if (null != progress) { + progress.start(); + } + final long size; + try { + size = doCopy(source, target, new byte[bufferSize(this.count)], progress); + target.flush(); + } catch (IOException e) { + throw new IORuntimeException(e); + } + + if (null != progress) { + progress.finish(); + } + + return size; + } + + /** + * 执行拷贝,如果限制最大长度,则按照最大长度读取,否则一直读取直到遇到-1 + * + * @param source {@link InputStream} + * @param target {@link OutputStream} + * @param buffer 缓存 + * @param progress 进度条 + * @return 拷贝总长度 + * @throws IOException IO异常 + */ + private long doCopy(InputStream source, OutputStream target, byte[] buffer, StreamProgress progress) throws IOException { + long numToRead = this.count > 0 ? this.count : Long.MAX_VALUE; + long total = 0; + + int read; + while (numToRead > 0) { + read = source.read(buffer, 0, bufferSize(numToRead)); + if (read < 0) { + // 提前读取到末尾 + break; + } + target.write(buffer, 0, read); + if(flushEveryBuffer){ + target.flush(); + } + + numToRead -= read; + total += read; + if (null != progress) { + progress.progress(this.count, total); + } + } + + return total; + } +} diff --git a/src/main/java/cn/hutool/core/io/copy/package-info.java b/src/main/java/cn/hutool/core/io/copy/package-info.java new file mode 100644 index 0000000..a544b56 --- /dev/null +++ b/src/main/java/cn/hutool/core/io/copy/package-info.java @@ -0,0 +1,7 @@ +/** + * IO流拷贝相关封装相关封装 + * + * @author looly + * @since 5.7.8 + */ +package cn.hutool.core.io.copy; diff --git a/src/main/java/cn/hutool/core/io/file/FileAppender.java b/src/main/java/cn/hutool/core/io/file/FileAppender.java new file mode 100644 index 0000000..20a97ee --- /dev/null +++ b/src/main/java/cn/hutool/core/io/file/FileAppender.java @@ -0,0 +1,127 @@ +package cn.hutool.core.io.file; + +import cn.hutool.core.thread.lock.LockUtil; +import cn.hutool.core.util.CharsetUtil; +import cn.hutool.core.util.ObjectUtil; + +import java.io.File; +import java.io.PrintWriter; +import java.io.Serializable; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.locks.Lock; + +/** + * 文件追加器
+ * 持有一个文件,在内存中积累一定量的数据后统一追加到文件
+ * 此类只有在写入文件时打开文件,并在写入结束后关闭之。因此此类不需要关闭
+ * 在调用append方法后会缓存于内存,只有超过容量后才会一次性写入文件,因此内存中随时有剩余未写入文件的内容,在最后必须调用flush方法将剩余内容刷入文件 + * + * @author looly + * @since 3.1.2 + */ +public class FileAppender implements Serializable { + private static final long serialVersionUID = 1L; + + private final FileWriter writer; + /** + * 内存中持有的字符串数 + */ + private final int capacity; + /** + * 追加内容是否为新行 + */ + private final boolean isNewLineMode; + /** + * 数据行缓存 + */ + private final List list; + /** + * 写出锁,用于保护写出线程安全 + */ + private final Lock lock; + + /** + * 构造 + * + * @param destFile 目标文件 + * @param capacity 当行数积累多少条时刷入到文件 + * @param isNewLineMode 追加内容是否为新行 + */ + public FileAppender(File destFile, int capacity, boolean isNewLineMode) { + this(destFile, CharsetUtil.CHARSET_UTF_8, capacity, isNewLineMode); + } + + /** + * 构造 + * + * @param destFile 目标文件 + * @param charset 编码 + * @param capacity 当行数积累多少条时刷入到文件 + * @param isNewLineMode 追加内容是否为新行 + */ + public FileAppender(File destFile, Charset charset, int capacity, boolean isNewLineMode) { + this(destFile, charset, capacity, isNewLineMode, null); + } + + /** + * 构造 + * + * @param destFile 目标文件 + * @param charset 编码 + * @param capacity 当行数积累多少条时刷入到文件 + * @param isNewLineMode 追加内容是否为新行 + * @param lock 是否加锁,添加则使用给定锁保护写出,保证线程安全,{@code null}则表示无锁 + */ + public FileAppender(File destFile, Charset charset, int capacity, boolean isNewLineMode, Lock lock) { + this.capacity = capacity; + this.list = new ArrayList<>(capacity); + this.isNewLineMode = isNewLineMode; + this.writer = FileWriter.create(destFile, charset); + this.lock = ObjectUtil.defaultIfNull(lock, LockUtil::getNoLock); + } + + /** + * 追加 + * + * @param line 行 + * @return this + */ + public FileAppender append(String line) { + if (list.size() >= capacity) { + flush(); + } + + this.lock.lock(); + try{ + list.add(line); + } finally { + this.lock.unlock(); + } + return this; + } + + /** + * 刷入到文件 + * + * @return this + */ + public FileAppender flush() { + this.lock.lock(); + try{ + try (PrintWriter pw = writer.getPrintWriter(true)) { + for (String str : list) { + pw.print(str); + if (isNewLineMode) { + pw.println(); + } + } + } + list.clear(); + } finally { + this.lock.unlock(); + } + return this; + } +} diff --git a/src/main/java/cn/hutool/core/io/file/FileCopier.java b/src/main/java/cn/hutool/core/io/file/FileCopier.java new file mode 100644 index 0000000..cd685e3 --- /dev/null +++ b/src/main/java/cn/hutool/core/io/file/FileCopier.java @@ -0,0 +1,291 @@ +package cn.hutool.core.io.file; + +import cn.hutool.core.io.FileUtil; +import cn.hutool.core.io.IORuntimeException; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.lang.copier.SrcToDestCopier; +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.StrUtil; + +import java.io.File; +import java.io.IOException; +import java.nio.file.CopyOption; +import java.nio.file.Files; +import java.nio.file.StandardCopyOption; +import java.util.ArrayList; + +/** + * 文件拷贝器
+ * 支持以下几种情况: + *
+ * 1、文件复制到文件
+ * 2、文件复制到目录
+ * 3、目录复制到目录
+ * 4、目录下的文件和目录复制到另一个目录
+ * 
+ * + * @author Looly + * @since 3.0.9 + */ +public class FileCopier extends SrcToDestCopier{ + private static final long serialVersionUID = 1L; + + /** 是否覆盖目标文件 */ + private boolean isOverride; + /** 是否拷贝所有属性 */ + private boolean isCopyAttributes; + /** 当拷贝来源是目录时是否只拷贝目录下的内容 */ + private boolean isCopyContentIfDir; + /** 当拷贝来源是目录时是否只拷贝文件而忽略子目录 */ + private boolean isOnlyCopyFile; + + //-------------------------------------------------------------------------------------------------------- static method start + /** + * 新建一个文件复制器 + * @param srcPath 源文件路径(相对ClassPath路径或绝对路径) + * @param destPath 目标文件路径(相对ClassPath路径或绝对路径) + * @return this + */ + public static FileCopier create(String srcPath, String destPath) { + return new FileCopier(FileUtil.file(srcPath), FileUtil.file(destPath)); + } + + /** + * 新建一个文件复制器 + * @param src 源文件 + * @param dest 目标文件 + * @return this + */ + public static FileCopier create(File src, File dest) { + return new FileCopier(src, dest); + } + //-------------------------------------------------------------------------------------------------------- static method end + + //-------------------------------------------------------------------------------------------------------- Constructor start + /** + * 构造 + * @param src 源文件 + * @param dest 目标文件 + */ + public FileCopier(File src, File dest) { + this.src = src; + this.dest = dest; + } + //-------------------------------------------------------------------------------------------------------- Constructor end + + //-------------------------------------------------------------------------------------------------------- Getters and Setters start + /** + * 是否覆盖目标文件 + * @return 是否覆盖目标文件 + */ + public boolean isOverride() { + return isOverride; + } + /** + * 设置是否覆盖目标文件 + * @param isOverride 是否覆盖目标文件 + * @return this + */ + public FileCopier setOverride(boolean isOverride) { + this.isOverride = isOverride; + return this; + } + + /** + * 是否拷贝所有属性 + * @return 是否拷贝所有属性 + */ + public boolean isCopyAttributes() { + return isCopyAttributes; + } + /** + * 设置是否拷贝所有属性 + * @param isCopyAttributes 是否拷贝所有属性 + * @return this + */ + public FileCopier setCopyAttributes(boolean isCopyAttributes) { + this.isCopyAttributes = isCopyAttributes; + return this; + } + + /** + * 当拷贝来源是目录时是否只拷贝目录下的内容 + * @return 当拷贝来源是目录时是否只拷贝目录下的内容 + */ + public boolean isCopyContentIfDir() { + return isCopyContentIfDir; + } + + /** + * 当拷贝来源是目录时是否只拷贝目录下的内容 + * @param isCopyContentIfDir 是否只拷贝目录下的内容 + * @return this + */ + public FileCopier setCopyContentIfDir(boolean isCopyContentIfDir) { + this.isCopyContentIfDir = isCopyContentIfDir; + return this; + } + + /** + * 当拷贝来源是目录时是否只拷贝文件而忽略子目录 + * + * @return 当拷贝来源是目录时是否只拷贝文件而忽略子目录 + * @since 4.1.5 + */ + public boolean isOnlyCopyFile() { + return isOnlyCopyFile; + } + + /** + * 设置当拷贝来源是目录时是否只拷贝文件而忽略子目录 + * + * @param isOnlyCopyFile 当拷贝来源是目录时是否只拷贝文件而忽略子目录 + * @return this + * @since 4.1.5 + */ + public FileCopier setOnlyCopyFile(boolean isOnlyCopyFile) { + this.isOnlyCopyFile = isOnlyCopyFile; + return this; + } + //-------------------------------------------------------------------------------------------------------- Getters and Setters end + + /** + * 执行拷贝
+ * 拷贝规则为: + *
+	 * 1、源为文件,目标为已存在目录,则拷贝到目录下,文件名不变
+	 * 2、源为文件,目标为不存在路径,则目标以文件对待(自动创建父级目录)比如:/dest/aaa,如果aaa不存在,则aaa被当作文件名
+	 * 3、源为文件,目标是一个已存在的文件,则当{@link #setOverride(boolean)}设为true时会被覆盖,默认不覆盖
+	 * 4、源为目录,目标为已存在目录,当{@link #setCopyContentIfDir(boolean)}为true时,只拷贝目录中的内容到目标目录中,否则整个源目录连同其目录拷贝到目标目录中
+	 * 5、源为目录,目标为不存在路径,则自动创建目标为新目录,然后按照规则4复制
+	 * 6、源为目录,目标为文件,抛出IO异常
+	 * 7、源路径和目标路径相同时,抛出IO异常
+	 * 
+ * + * @return 拷贝后目标的文件或目录 + * @throws IORuntimeException IO异常 + */ + @Override + public File copy() throws IORuntimeException{ + final File src = this.src; + File dest = this.dest; + // check + Assert.notNull(src, "Source File is null !"); + if (!src.exists()) { + throw new IORuntimeException("File not exist: " + src); + } + Assert.notNull(dest, "Destination File or directiory is null !"); + if (FileUtil.equals(src, dest)) { + throw new IORuntimeException("Files '{}' and '{}' are equal", src, dest); + } + + if (src.isDirectory()) {// 复制目录 + if(dest.exists() && !dest.isDirectory()) { + //源为目录,目标为文件,抛出IO异常 + throw new IORuntimeException("Src is a directory but dest is a file!"); + } + if(FileUtil.isSub(src, dest)) { + throw new IORuntimeException("Dest is a sub directory of src !"); + } + + final File subTarget = isCopyContentIfDir ? dest : FileUtil.mkdir(FileUtil.file(dest, src.getName())); + internalCopyDirContent(src, subTarget); + } else {// 复制文件 + dest = internalCopyFile(src, dest); + } + return dest; + } + + //----------------------------------------------------------------------------------------- Private method start + /** + * 拷贝目录内容,只用于内部,不做任何安全检查
+ * 拷贝内容的意思为源目录下的所有文件和目录拷贝到另一个目录下,而不拷贝源目录本身 + * + * @param src 源目录 + * @param dest 目标目录 + * @throws IORuntimeException IO异常 + */ + private void internalCopyDirContent(File src, File dest) throws IORuntimeException { + if (null != copyFilter && !copyFilter.accept(src)) { + //被过滤的目录跳过 + return; + } + + if (!dest.exists()) { + //目标为不存在路径,创建为目录 + //noinspection ResultOfMethodCallIgnored + dest.mkdirs(); + } else if (!dest.isDirectory()) { + throw new IORuntimeException(StrUtil.format("Src [{}] is a directory but dest [{}] is a file!", src.getPath(), dest.getPath())); + } + + final String[] files = src.list(); + if(ArrayUtil.isNotEmpty(files)){ + File srcFile; + File destFile; + for (String file : files) { + srcFile = new File(src, file); + destFile = this.isOnlyCopyFile ? dest : new File(dest, file); + // 递归复制 + if (srcFile.isDirectory()) { + internalCopyDirContent(srcFile, destFile); + } else { + internalCopyFile(srcFile, destFile); + } + } + } + } + + /** + * 拷贝文件,只用于内部,不做任何安全检查
+ * 情况如下: + *
+	 * 1、如果目标是一个不存在的路径,则目标以文件对待(自动创建父级目录)比如:/dest/aaa,如果aaa不存在,则aaa被当作文件名
+	 * 2、如果目标是一个已存在的目录,则文件拷贝到此目录下,文件名与原文件名一致
+	 * 
+ * + * @param src 源文件,必须为文件 + * @param dest 目标文件,如果非覆盖模式必须为目录 + * @return 目标的目录或文件 + * @throws IORuntimeException IO异常 + */ + private File internalCopyFile(File src, File dest) throws IORuntimeException { + if (null != copyFilter && !copyFilter.accept(src)) { + //被过滤的文件跳过 + return src; + } + + // 如果已经存在目标文件,切为不覆盖模式,跳过之 + if (dest.exists()) { + if(dest.isDirectory()) { + //目标为目录,目录下创建同名文件 + dest = new File(dest, src.getName()); + } + + if(dest.exists() && !isOverride) { + //非覆盖模式跳过 + return src; + } + }else { + //路径不存在则创建父目录 + FileUtil.mkParentDirs(dest); + } + + final ArrayList optionList = new ArrayList<>(2); + if(isOverride) { + optionList.add(StandardCopyOption.REPLACE_EXISTING); + } + if(isCopyAttributes) { + optionList.add(StandardCopyOption.COPY_ATTRIBUTES); + } + + try { + Files.copy(src.toPath(), dest.toPath(), optionList.toArray(new CopyOption[0])); + } catch (IOException e) { + throw new IORuntimeException(e); + } + + return dest; + } + //----------------------------------------------------------------------------------------- Private method end +} diff --git a/src/main/java/cn/hutool/core/io/file/FileMode.java b/src/main/java/cn/hutool/core/io/file/FileMode.java new file mode 100644 index 0000000..a58020b --- /dev/null +++ b/src/main/java/cn/hutool/core/io/file/FileMode.java @@ -0,0 +1,19 @@ +package cn.hutool.core.io.file; + +/** + * 文件读写模式,常用于RandomAccessFile + * + * @author looly + * @since 4.5.2 + */ +public enum FileMode { + /** 以只读方式打开。调用结果对象的任何 write 方法都将导致抛出 IOException。 */ + r, + /** 打开以便读取和写入。 */ + rw, + /** 打开以便读取和写入。相对于 "rw","rws" 还要求对“文件的内容”或“元数据”的每个更新都同步写入到基础存储设备。 */ + rws, + /** 打开以便读取和写入,相对于 "rw","rwd" 还要求对“文件的内容”的每个更新都同步写入到基础存储设备。 */ + rwd + +} diff --git a/src/main/java/cn/hutool/core/io/file/FileNameUtil.java b/src/main/java/cn/hutool/core/io/file/FileNameUtil.java new file mode 100644 index 0000000..c86ed71 --- /dev/null +++ b/src/main/java/cn/hutool/core/io/file/FileNameUtil.java @@ -0,0 +1,285 @@ +package cn.hutool.core.io.file; + +import cn.hutool.core.util.CharUtil; +import cn.hutool.core.util.ReUtil; +import cn.hutool.core.util.StrUtil; + +import java.io.File; +import java.util.regex.Pattern; + +/** + * 文件名相关工具类 + * + * @author looly + * @since 5.4.1 + */ +public class FileNameUtil { + + /** + * .java文件扩展名 + */ + public static final String EXT_JAVA = ".java"; + /** + * .class文件扩展名 + */ + public static final String EXT_CLASS = ".class"; + /** + * .jar文件扩展名 + */ + public static final String EXT_JAR = ".jar"; + + /** + * 类Unix路径分隔符 + */ + public static final char UNIX_SEPARATOR = CharUtil.SLASH; + /** + * Windows路径分隔符 + */ + public static final char WINDOWS_SEPARATOR = CharUtil.BACKSLASH; + + /** + * Windows下文件名中的无效字符 + */ + private static final Pattern FILE_NAME_INVALID_PATTERN_WIN = Pattern.compile("[\\\\/:*?\"<>|\r\n]"); + + /** + * 特殊后缀 + */ + private static final CharSequence[] SPECIAL_SUFFIX = {"tar.bz2", "tar.Z", "tar.gz", "tar.xz"}; + + + // -------------------------------------------------------------------------------------------- name start + + /** + * 返回文件名 + * + * @param file 文件 + * @return 文件名 + * @since 4.1.13 + */ + public static String getName(File file) { + return (null != file) ? file.getName() : null; + } + + /** + * 返回文件名
+ *
+	 * "d:/test/aaa" 返回 "aaa"
+	 * "/test/aaa.jpg" 返回 "aaa.jpg"
+	 * 
+ * + * @param filePath 文件 + * @return 文件名 + * @since 4.1.13 + */ + public static String getName(String filePath) { + if (null == filePath) { + return null; + } + int len = filePath.length(); + if (0 == len) { + return filePath; + } + if (CharUtil.isFileSeparator(filePath.charAt(len - 1))) { + // 以分隔符结尾的去掉结尾分隔符 + len--; + } + + int begin = 0; + char c; + for (int i = len - 1; i > -1; i--) { + c = filePath.charAt(i); + if (CharUtil.isFileSeparator(c)) { + // 查找最后一个路径分隔符(/或者\) + begin = i + 1; + break; + } + } + + return filePath.substring(begin, len); + } + + /** + * 获取文件后缀名,扩展名不带“.” + * + * @param file 文件 + * @return 扩展名 + * @see #extName(File) + * @since 5.3.8 + */ + public static String getSuffix(File file) { + return extName(file); + } + + /** + * 获得文件后缀名,扩展名不带“.” + * + * @param fileName 文件名 + * @return 扩展名 + * @see #extName(String) + * @since 5.3.8 + */ + public static String getSuffix(String fileName) { + return extName(fileName); + } + + /** + * 返回主文件名 + * + * @param file 文件 + * @return 主文件名 + * @see #mainName(File) + * @since 5.3.8 + */ + public static String getPrefix(File file) { + return mainName(file); + } + + /** + * 返回主文件名 + * + * @param fileName 完整文件名 + * @return 主文件名 + * @see #mainName(String) + * @since 5.3.8 + */ + public static String getPrefix(String fileName) { + return mainName(fileName); + } + + /** + * 返回主文件名 + * + * @param file 文件 + * @return 主文件名 + */ + public static String mainName(File file) { + if (file.isDirectory()) { + return file.getName(); + } + return mainName(file.getName()); + } + + /** + * 返回主文件名 + * + * @param fileName 完整文件名 + * @return 主文件名 + */ + public static String mainName(String fileName) { + if (null == fileName) { + return null; + } + int len = fileName.length(); + if (0 == len) { + return fileName; + } + + //issue#2642,多级扩展名的主文件名 + for (final CharSequence specialSuffix : SPECIAL_SUFFIX) { + if(StrUtil.endWith(fileName, "." + specialSuffix)){ + return StrUtil.subPre(fileName, len - specialSuffix.length() - 1); + } + } + + if (CharUtil.isFileSeparator(fileName.charAt(len - 1))) { + len--; + } + + int begin = 0; + int end = len; + char c; + for (int i = len - 1; i >= 0; i--) { + c = fileName.charAt(i); + if (len == end && CharUtil.DOT == c) { + // 查找最后一个文件名和扩展名的分隔符:. + end = i; + } + // 查找最后一个路径分隔符(/或者\),如果这个分隔符在.之后,则继续查找,否则结束 + if (CharUtil.isFileSeparator(c)) { + begin = i + 1; + break; + } + } + + return fileName.substring(begin, end); + } + + /** + * 获取文件扩展名(后缀名),扩展名不带“.” + * + * @param file 文件 + * @return 扩展名 + */ + public static String extName(File file) { + if (null == file) { + return null; + } + if (file.isDirectory()) { + return null; + } + return extName(file.getName()); + } + + /** + * 获得文件的扩展名(后缀名),扩展名不带“.” + * + * @param fileName 文件名 + * @return 扩展名 + */ + public static String extName(String fileName) { + if (fileName == null) { + return null; + } + final int index = fileName.lastIndexOf(StrUtil.DOT); + if (index == -1) { + return StrUtil.EMPTY; + } else { + // issue#I4W5FS@Gitee + final int secondToLastIndex = fileName.substring(0, index).lastIndexOf(StrUtil.DOT); + final String substr = fileName.substring(secondToLastIndex == -1 ? index : secondToLastIndex + 1); + if (StrUtil.containsAny(substr, SPECIAL_SUFFIX)) { + return substr; + } + + final String ext = fileName.substring(index + 1); + // 扩展名中不能包含路径相关的符号 + return StrUtil.containsAny(ext, UNIX_SEPARATOR, WINDOWS_SEPARATOR) ? StrUtil.EMPTY : ext; + } + } + + /** + * 清除文件名中的在Windows下不支持的非法字符,包括: \ / : * ? " < > | + * + * @param fileName 文件名(必须不包括路径,否则路径符将被替换) + * @return 清理后的文件名 + * @since 3.3.1 + */ + public static String cleanInvalid(String fileName) { + return StrUtil.isBlank(fileName) ? fileName : ReUtil.delAll(FILE_NAME_INVALID_PATTERN_WIN, fileName); + } + + /** + * 文件名中是否包含在Windows下不支持的非法字符,包括: \ / : * ? " < > | + * + * @param fileName 文件名(必须不包括路径,否则路径符将被替换) + * @return 是否包含非法字符 + * @since 3.3.1 + */ + public static boolean containsInvalid(String fileName) { + return (!StrUtil.isBlank(fileName)) && ReUtil.contains(FILE_NAME_INVALID_PATTERN_WIN, fileName); + } + + /** + * 根据文件名检查文件类型,忽略大小写 + * + * @param fileName 文件名,例如hutool.png + * @param extNames 被检查的扩展名数组,同一文件类型可能有多种扩展名,扩展名不带“.” + * @return 是否是指定扩展名的类型 + * @since 5.5.2 + */ + public static boolean isType(String fileName, String... extNames) { + return StrUtil.equalsAnyIgnoreCase(extName(fileName), extNames); + } + // -------------------------------------------------------------------------------------------- name end +} diff --git a/src/main/java/cn/hutool/core/io/file/FileReader.java b/src/main/java/cn/hutool/core/io/file/FileReader.java new file mode 100644 index 0000000..5d0c100 --- /dev/null +++ b/src/main/java/cn/hutool/core/io/file/FileReader.java @@ -0,0 +1,306 @@ +package cn.hutool.core.io.file; + +import cn.hutool.core.io.FileUtil; +import cn.hutool.core.io.IORuntimeException; +import cn.hutool.core.io.IoUtil; +import cn.hutool.core.io.LineHandler; +import cn.hutool.core.util.CharsetUtil; +import cn.hutool.core.util.StrUtil; + +import java.io.BufferedInputStream; +import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +/** + * 文件读取器 + * + * @author Looly + * + */ +public class FileReader extends FileWrapper { + private static final long serialVersionUID = 1L; + + /** + * 创建 FileReader + * @param file 文件 + * @param charset 编码,使用 {@link CharsetUtil} + * @return FileReader + */ + public static FileReader create(File file, Charset charset){ + return new FileReader(file, charset); + } + + /** + * 创建 FileReader, 编码:{@link FileWrapper#DEFAULT_CHARSET} + * @param file 文件 + * @return FileReader + */ + public static FileReader create(File file){ + return new FileReader(file); + } + + // ------------------------------------------------------- Constructor start + /** + * 构造 + * @param file 文件 + * @param charset 编码,使用 {@link CharsetUtil} + */ + public FileReader(File file, Charset charset) { + super(file, charset); + checkFile(); + } + + /** + * 构造 + * @param file 文件 + * @param charset 编码,使用 {@link CharsetUtil#charset(String)} + */ + public FileReader(File file, String charset) { + this(file, CharsetUtil.charset(charset)); + } + + /** + * 构造 + * @param filePath 文件路径,相对路径会被转换为相对于ClassPath的路径 + * @param charset 编码,使用 {@link CharsetUtil} + */ + public FileReader(String filePath, Charset charset) { + this(FileUtil.file(filePath), charset); + } + + /** + * 构造 + * @param filePath 文件路径,相对路径会被转换为相对于ClassPath的路径 + * @param charset 编码,使用 {@link CharsetUtil#charset(String)} + */ + public FileReader(String filePath, String charset) { + this(FileUtil.file(filePath), CharsetUtil.charset(charset)); + } + + /** + * 构造
+ * 编码使用 {@link FileWrapper#DEFAULT_CHARSET} + * @param file 文件 + */ + public FileReader(File file) { + this(file, DEFAULT_CHARSET); + } + + /** + * 构造
+ * 编码使用 {@link FileWrapper#DEFAULT_CHARSET} + * @param filePath 文件路径,相对路径会被转换为相对于ClassPath的路径 + */ + public FileReader(String filePath) { + this(filePath, DEFAULT_CHARSET); + } + // ------------------------------------------------------- Constructor end + + /** + * 读取文件所有数据
+ * 文件的长度不能超过 {@link Integer#MAX_VALUE} + * + * @return 字节码 + * @throws IORuntimeException IO异常 + */ + public byte[] readBytes() throws IORuntimeException { + long len = file.length(); + if (len >= Integer.MAX_VALUE) { + throw new IORuntimeException("File is larger then max array size"); + } + + byte[] bytes = new byte[(int) len]; + FileInputStream in = null; + int readLength; + try { + in = new FileInputStream(file); + readLength = in.read(bytes); + if(readLength < len){ + throw new IOException(StrUtil.format("File length is [{}] but read [{}]!", len, readLength)); + } + } catch (Exception e) { + throw new IORuntimeException(e); + } finally { + IoUtil.close(in); + } + + return bytes; + } + + /** + * 读取文件内容 + * + * @return 内容 + * @throws IORuntimeException IO异常 + */ + public String readString() throws IORuntimeException{ + return new String(readBytes(), this.charset); + } + + /** + * 从文件中读取每一行数据 + * + * @param 集合类型 + * @param collection 集合 + * @return 文件中的每行内容的集合 + * @throws IORuntimeException IO异常 + */ + public > T readLines(T collection) throws IORuntimeException { + BufferedReader reader = null; + try { + reader = FileUtil.getReader(file, charset); + String line; + while (true) { + line = reader.readLine(); + if (line == null) { + break; + } + collection.add(line); + } + return collection; + } catch (IOException e) { + throw new IORuntimeException(e); + } finally { + IoUtil.close(reader); + } + } + + /** + * 按照行处理文件内容 + * + * @param lineHandler 行处理器 + * @throws IORuntimeException IO异常 + * @since 3.0.9 + */ + public void readLines(LineHandler lineHandler) throws IORuntimeException{ + BufferedReader reader = null; + try { + reader = FileUtil.getReader(file, charset); + IoUtil.readLines(reader, lineHandler); + } finally { + IoUtil.close(reader); + } + } + + /** + * 从文件中读取每一行数据 + * + * @return 文件中的每行内容的集合 + * @throws IORuntimeException IO异常 + */ + public List readLines() throws IORuntimeException { + return readLines(new ArrayList<>()); + } + + /** + * 按照给定的readerHandler读取文件中的数据 + * + * @param 读取的结果对象类型 + * @param readerHandler Reader处理类 + * @return 从文件中read出的数据 + * @throws IORuntimeException IO异常 + */ + public T read(ReaderHandler readerHandler) throws IORuntimeException { + BufferedReader reader = null; + T result; + try { + reader = FileUtil.getReader(this.file, charset); + result = readerHandler.handle(reader); + } catch (IOException e) { + throw new IORuntimeException(e); + } finally { + IoUtil.close(reader); + } + return result; + } + + /** + * 获得一个文件读取器 + * + * @return BufferedReader对象 + * @throws IORuntimeException IO异常 + */ + public BufferedReader getReader() throws IORuntimeException { + return IoUtil.getReader(getInputStream(), this.charset); + } + + /** + * 获得输入流 + * + * @return 输入流 + * @throws IORuntimeException IO异常 + */ + public BufferedInputStream getInputStream() throws IORuntimeException { + try { + return new BufferedInputStream(new FileInputStream(this.file)); + } catch (IOException e) { + throw new IORuntimeException(e); + } + } + + /** + * 将文件写入流中,此方法不会关闭比输出流 + * + * @param out 流 + * @return 写出的流byte数 + * @throws IORuntimeException IO异常 + */ + public long writeToStream(OutputStream out) throws IORuntimeException { + return writeToStream(out, false); + } + + /** + * 将文件写入流中 + * + * @param out 流 + * @param isCloseOut 是否关闭输出流 + * @return 写出的流byte数 + * @throws IORuntimeException IO异常 + * @since 5.5.2 + */ + public long writeToStream(OutputStream out, boolean isCloseOut) throws IORuntimeException { + try (FileInputStream in = new FileInputStream(this.file)){ + return IoUtil.copy(in, out); + }catch (IOException e) { + throw new IORuntimeException(e); + } finally{ + if(isCloseOut){ + IoUtil.close(out); + } + } + } + + // -------------------------------------------------------------------------- Interface start + /** + * Reader处理接口 + * + * @author Luxiaolei + * + * @param Reader处理返回结果类型 + */ + public interface ReaderHandler { + T handle(BufferedReader reader) throws IOException; + } + // -------------------------------------------------------------------------- Interface end + + /** + * 检查文件 + * + * @throws IORuntimeException IO异常 + */ + private void checkFile() throws IORuntimeException { + if (!file.exists()) { + throw new IORuntimeException("File not exist: " + file); + } + if (!file.isFile()) { + throw new IORuntimeException("Not a file:" + file); + } + } +} diff --git a/src/main/java/cn/hutool/core/io/file/FileSystemUtil.java b/src/main/java/cn/hutool/core/io/file/FileSystemUtil.java new file mode 100644 index 0000000..502bf09 --- /dev/null +++ b/src/main/java/cn/hutool/core/io/file/FileSystemUtil.java @@ -0,0 +1,84 @@ +package cn.hutool.core.io.file; + +import cn.hutool.core.io.IORuntimeException; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.CharsetUtil; +import cn.hutool.core.util.StrUtil; + +import java.io.IOException; +import java.net.URI; +import java.nio.charset.Charset; +import java.nio.file.FileSystem; +import java.nio.file.FileSystems; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.HashMap; + +/** + * {@link FileSystem}相关工具类封装
+ * 参考:https://blog.csdn.net/j16421881/article/details/78858690 + * + * @author looly + * @since 5.7.15 + */ +public class FileSystemUtil { + + /** + * 创建 {@link FileSystem} + * + * @param path 文件路径,可以是目录或Zip文件等 + * @return {@link FileSystem} + */ + public static FileSystem create(String path) { + try { + return FileSystems.newFileSystem( + Paths.get(path).toUri(), + MapUtil.of("create", "true")); + } catch (IOException e) { + throw new IORuntimeException(e); + } + } + + /** + * 创建 Zip的{@link FileSystem},默认UTF-8编码 + * + * @param path 文件路径,可以是目录或Zip文件等 + * @return {@link FileSystem} + */ + public static FileSystem createZip(String path) { + return createZip(path, null); + } + + /** + * 创建 Zip的{@link FileSystem} + * + * @param path 文件路径,可以是目录或Zip文件等 + * @param charset 编码 + * @return {@link FileSystem} + */ + public static FileSystem createZip(String path, Charset charset) { + if(null == charset){ + charset = CharsetUtil.CHARSET_UTF_8; + } + final HashMap env = new HashMap<>(); + env.put("create", "true"); + env.put("encoding", charset.name()); + + try { + return FileSystems.newFileSystem( + URI.create("jar:" + Paths.get(path).toUri()), env); + } catch (IOException e) { + throw new IORuntimeException(e); + } + } + + /** + * 获取目录的根路径,或Zip文件中的根路径 + * + * @param fileSystem {@link FileSystem} + * @return 根 {@link Path} + */ + public static Path getRoot(FileSystem fileSystem) { + return fileSystem.getPath(StrUtil.SLASH); + } +} diff --git a/src/main/java/cn/hutool/core/io/file/FileWrapper.java b/src/main/java/cn/hutool/core/io/file/FileWrapper.java new file mode 100644 index 0000000..cb5fb62 --- /dev/null +++ b/src/main/java/cn/hutool/core/io/file/FileWrapper.java @@ -0,0 +1,83 @@ +package cn.hutool.core.io.file; + +import java.io.File; +import java.io.Serializable; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; + +import cn.hutool.core.io.FileUtil; +import cn.hutool.core.util.CharsetUtil; + +/** + * 文件包装器,扩展文件对象 + * + * @author Looly + * + */ +public class FileWrapper implements Serializable{ + private static final long serialVersionUID = 1L; + + protected File file; + protected Charset charset; + + /** 默认编码:UTF-8 */ + public static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8; + + // ------------------------------------------------------- Constructor start + /** + * 构造 + * @param file 文件 + * @param charset 编码,使用 {@link CharsetUtil} + */ + public FileWrapper(File file, Charset charset) { + this.file = file; + this.charset = charset; + } + // ------------------------------------------------------- Constructor end + + // ------------------------------------------------------- Setters and Getters start start + /** + * 获得文件 + * @return 文件 + */ + public File getFile() { + return file; + } + + /** + * 设置文件 + * @param file 文件 + * @return 自身 + */ + public FileWrapper setFile(File file) { + this.file = file; + return this; + } + + /** + * 获得字符集编码 + * @return 编码 + */ + public Charset getCharset() { + return charset; + } + + /** + * 设置字符集编码 + * @param charset 编码 + * @return 自身 + */ + public FileWrapper setCharset(Charset charset) { + this.charset = charset; + return this; + } + // ------------------------------------------------------- Setters and Getters start end + + /** + * 可读的文件大小 + * @return 大小 + */ + public String readableFileSize() { + return FileUtil.readableFileSize(file.length()); + } +} diff --git a/src/main/java/cn/hutool/core/io/file/FileWriter.java b/src/main/java/cn/hutool/core/io/file/FileWriter.java new file mode 100644 index 0000000..f31adba --- /dev/null +++ b/src/main/java/cn/hutool/core/io/file/FileWriter.java @@ -0,0 +1,424 @@ +package cn.hutool.core.io.file; + +import cn.hutool.core.io.FileUtil; +import cn.hutool.core.io.IORuntimeException; +import cn.hutool.core.io.IoUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.CharsetUtil; +import cn.hutool.core.util.StrUtil; + +import java.io.BufferedOutputStream; +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; +import java.nio.charset.Charset; +import java.nio.file.Files; +import java.util.Map; +import java.util.Map.Entry; + +/** + * 文件写入器 + * + * @author Looly + */ +public class FileWriter extends FileWrapper { + private static final long serialVersionUID = 1L; + + /** + * 创建 FileWriter + * + * @param file 文件 + * @param charset 编码,使用 {@link CharsetUtil} + * @return FileWriter + */ + public static FileWriter create(File file, Charset charset) { + return new FileWriter(file, charset); + } + + /** + * 创建 FileWriter, 编码:{@link FileWrapper#DEFAULT_CHARSET} + * + * @param file 文件 + * @return FileWriter + */ + public static FileWriter create(File file) { + return new FileWriter(file); + } + + // ------------------------------------------------------- Constructor start + + /** + * 构造 + * + * @param file 文件 + * @param charset 编码,使用 {@link CharsetUtil} + */ + public FileWriter(File file, Charset charset) { + super(file, charset); + checkFile(); + } + + /** + * 构造 + * + * @param file 文件 + * @param charset 编码,使用 {@link CharsetUtil#charset(String)} + */ + public FileWriter(File file, String charset) { + this(file, CharsetUtil.charset(charset)); + } + + /** + * 构造 + * + * @param filePath 文件路径,相对路径会被转换为相对于ClassPath的路径 + * @param charset 编码,使用 {@link CharsetUtil} + */ + public FileWriter(String filePath, Charset charset) { + this(FileUtil.file(filePath), charset); + } + + /** + * 构造 + * + * @param filePath 文件路径,相对路径会被转换为相对于ClassPath的路径 + * @param charset 编码,使用 {@link CharsetUtil#charset(String)} + */ + public FileWriter(String filePath, String charset) { + this(FileUtil.file(filePath), CharsetUtil.charset(charset)); + } + + /** + * 构造
+ * 编码使用 {@link FileWrapper#DEFAULT_CHARSET} + * + * @param file 文件 + */ + public FileWriter(File file) { + this(file, DEFAULT_CHARSET); + } + + /** + * 构造
+ * 编码使用 {@link FileWrapper#DEFAULT_CHARSET} + * + * @param filePath 文件路径,相对路径会被转换为相对于ClassPath的路径 + */ + public FileWriter(String filePath) { + this(filePath, DEFAULT_CHARSET); + } + // ------------------------------------------------------- Constructor end + + /** + * 将String写入文件 + * + * @param content 写入的内容 + * @param isAppend 是否追加 + * @return 目标文件 + * @throws IORuntimeException IO异常 + */ + public File write(String content, boolean isAppend) throws IORuntimeException { + BufferedWriter writer = null; + try { + writer = getWriter(isAppend); + writer.write(content); + writer.flush(); + } catch (IOException e) { + throw new IORuntimeException(e); + } finally { + IoUtil.close(writer); + } + return file; + } + + /** + * 将String写入文件,覆盖模式 + * + * @param content 写入的内容 + * @return 目标文件 + * @throws IORuntimeException IO异常 + */ + public File write(String content) throws IORuntimeException { + return write(content, false); + } + + /** + * 将String写入文件,追加模式 + * + * @param content 写入的内容 + * @return 写入的文件 + * @throws IORuntimeException IO异常 + */ + public File append(String content) throws IORuntimeException { + return write(content, true); + } + + /** + * 将列表写入文件,覆盖模式 + * + * @param 集合元素类型 + * @param list 列表 + * @return 目标文件 + * @throws IORuntimeException IO异常 + */ + public File writeLines(Iterable list) throws IORuntimeException { + return writeLines(list, false); + } + + /** + * 将列表写入文件,追加模式 + * + * @param 集合元素类型 + * @param list 列表 + * @return 目标文件 + * @throws IORuntimeException IO异常 + */ + public File appendLines(Iterable list) throws IORuntimeException { + return writeLines(list, true); + } + + /** + * 将列表写入文件 + * + * @param 集合元素类型 + * @param list 列表 + * @param isAppend 是否追加 + * @return 目标文件 + * @throws IORuntimeException IO异常 + */ + public File writeLines(Iterable list, boolean isAppend) throws IORuntimeException { + return writeLines(list, null, isAppend); + } + + /** + * 将列表写入文件 + * + * @param 集合元素类型 + * @param list 列表 + * @param lineSeparator 换行符枚举(Windows、Mac或Linux换行符) + * @param isAppend 是否追加 + * @return 目标文件 + * @throws IORuntimeException IO异常 + * @since 3.1.0 + */ + public File writeLines(Iterable list, LineSeparator lineSeparator, boolean isAppend) throws IORuntimeException { + try (PrintWriter writer = getPrintWriter(isAppend)) { + boolean isFirst = true; + for (T t : list) { + if (null != t) { + if(isFirst){ + isFirst = false; + if(isAppend && FileUtil.isNotEmpty(this.file)){ + // 追加模式下且文件非空,补充换行符 + printNewLine(writer, lineSeparator); + } + } else{ + printNewLine(writer, lineSeparator); + } + writer.print(t); + + writer.flush(); + } + } + } + return this.file; + } + + /** + * 将Map写入文件,每个键值对为一行,一行中键与值之间使用kvSeparator分隔 + * + * @param map Map + * @param kvSeparator 键和值之间的分隔符,如果传入null使用默认分隔符" = " + * @param isAppend 是否追加 + * @return 目标文件 + * @throws IORuntimeException IO异常 + * @since 4.0.5 + */ + public File writeMap(Map map, String kvSeparator, boolean isAppend) throws IORuntimeException { + return writeMap(map, null, kvSeparator, isAppend); + } + + /** + * 将Map写入文件,每个键值对为一行,一行中键与值之间使用kvSeparator分隔 + * + * @param map Map + * @param lineSeparator 换行符枚举(Windows、Mac或Linux换行符) + * @param kvSeparator 键和值之间的分隔符,如果传入null使用默认分隔符" = " + * @param isAppend 是否追加 + * @return 目标文件 + * @throws IORuntimeException IO异常 + * @since 4.0.5 + */ + public File writeMap(Map map, LineSeparator lineSeparator, String kvSeparator, boolean isAppend) throws IORuntimeException { + if (null == kvSeparator) { + kvSeparator = " = "; + } + try (PrintWriter writer = getPrintWriter(isAppend)) { + for (Entry entry : map.entrySet()) { + if (null != entry) { + writer.print(StrUtil.format("{}{}{}", entry.getKey(), kvSeparator, entry.getValue())); + printNewLine(writer, lineSeparator); + writer.flush(); + } + } + } + return this.file; + } + + /** + * 写入数据到文件 + * + * @param data 数据 + * @param off 数据开始位置 + * @param len 数据长度 + * @return 目标文件 + * @throws IORuntimeException IO异常 + */ + public File write(byte[] data, int off, int len) throws IORuntimeException { + return write(data, off, len, false); + } + + /** + * 追加数据到文件 + * + * @param data 数据 + * @param off 数据开始位置 + * @param len 数据长度 + * @return 目标文件 + * @throws IORuntimeException IO异常 + */ + public File append(byte[] data, int off, int len) throws IORuntimeException { + return write(data, off, len, true); + } + + /** + * 写入数据到文件 + * + * @param data 数据 + * @param off 数据开始位置 + * @param len 数据长度 + * @param isAppend 是否追加模式 + * @return 目标文件 + * @throws IORuntimeException IO异常 + */ + public File write(byte[] data, int off, int len, boolean isAppend) throws IORuntimeException { + try (FileOutputStream out = new FileOutputStream(FileUtil.touch(file), isAppend)) { + out.write(data, off, len); + out.flush(); + } catch (IOException e) { + throw new IORuntimeException(e); + } + return file; + } + + /** + * 将流的内容写入文件
+ * 此方法会自动关闭输入流 + * + * @param in 输入流,不关闭 + * @return dest + * @throws IORuntimeException IO异常 + */ + public File writeFromStream(InputStream in) throws IORuntimeException { + return writeFromStream(in, true); + } + + /** + * 将流的内容写入文件 + * + * @param in 输入流,不关闭 + * @param isCloseIn 是否关闭输入流 + * @return dest + * @throws IORuntimeException IO异常 + * @since 5.5.2 + */ + public File writeFromStream(InputStream in, boolean isCloseIn) throws IORuntimeException { + OutputStream out = null; + try { + out = Files.newOutputStream(FileUtil.touch(file).toPath()); + IoUtil.copy(in, out); + } catch (final IOException e) { + throw new IORuntimeException(e); + } finally { + IoUtil.close(out); + if (isCloseIn) { + IoUtil.close(in); + } + } + return file; + } + + /** + * 获得一个输出流对象 + * + * @return 输出流对象 + * @throws IORuntimeException IO异常 + */ + public BufferedOutputStream getOutputStream() throws IORuntimeException { + try { + return new BufferedOutputStream(Files.newOutputStream(FileUtil.touch(file).toPath())); + } catch (IOException e) { + throw new IORuntimeException(e); + } + } + + /** + * 获得一个带缓存的写入对象 + * + * @param isAppend 是否追加 + * @return BufferedReader对象 + * @throws IORuntimeException IO异常 + */ + public BufferedWriter getWriter(boolean isAppend) throws IORuntimeException { + try { + return new BufferedWriter(new OutputStreamWriter(new FileOutputStream(FileUtil.touch(file), isAppend), charset)); + } catch (Exception e) { + throw new IORuntimeException(e); + } + } + + /** + * 获得一个打印写入对象,可以有print + * + * @param isAppend 是否追加 + * @return 打印对象 + * @throws IORuntimeException IO异常 + */ + public PrintWriter getPrintWriter(boolean isAppend) throws IORuntimeException { + return new PrintWriter(getWriter(isAppend)); + } + + /** + * 检查文件 + * + * @throws IORuntimeException IO异常 + */ + private void checkFile() throws IORuntimeException { + Assert.notNull(file, "File to write content is null !"); + if (this.file.exists() && !file.isFile()) { + throw new IORuntimeException("File [{}] is not a file !", this.file.getAbsoluteFile()); + } + } + + /** + * 打印新行 + * + * @param writer Writer + * @param lineSeparator 换行符枚举 + * @since 4.0.5 + */ + private void printNewLine(PrintWriter writer, LineSeparator lineSeparator) { + if (null == lineSeparator) { + //默认换行符 + writer.println(); + } else { + //自定义换行符 + writer.print(lineSeparator.getValue()); + } + } +} diff --git a/src/main/java/cn/hutool/core/io/file/LineReadWatcher.java b/src/main/java/cn/hutool/core/io/file/LineReadWatcher.java new file mode 100644 index 0000000..1c9e500 --- /dev/null +++ b/src/main/java/cn/hutool/core/io/file/LineReadWatcher.java @@ -0,0 +1,71 @@ +package cn.hutool.core.io.file; + +import cn.hutool.core.io.FileUtil; +import cn.hutool.core.io.IORuntimeException; +import cn.hutool.core.io.LineHandler; +import cn.hutool.core.io.watch.SimpleWatcher; + +import java.io.IOException; +import java.io.RandomAccessFile; +import java.nio.charset.Charset; +import java.nio.file.Path; +import java.nio.file.WatchEvent; + +/** + * 行处理的Watcher实现 + * + * @author looly + * @since 4.5.2 + */ +public class LineReadWatcher extends SimpleWatcher implements Runnable { + + private final RandomAccessFile randomAccessFile; + private final Charset charset; + private final LineHandler lineHandler; + + /** + * 构造 + * + * @param randomAccessFile {@link RandomAccessFile} + * @param charset 编码 + * @param lineHandler 行处理器{@link LineHandler}实现 + */ + public LineReadWatcher(RandomAccessFile randomAccessFile, Charset charset, LineHandler lineHandler) { + this.randomAccessFile = randomAccessFile; + this.charset = charset; + this.lineHandler = lineHandler; + } + + @Override + public void run() { + onModify(null, null); + } + + @Override + public void onModify(WatchEvent event, Path currentPath) { + final RandomAccessFile randomAccessFile = this.randomAccessFile; + final Charset charset = this.charset; + final LineHandler lineHandler = this.lineHandler; + + try { + final long currentLength = randomAccessFile.length(); + final long position = randomAccessFile.getFilePointer(); + if (position == currentLength) { + // 内容长度不变时忽略此次事件 + return; + } else if (currentLength < position) { + // 如果内容变短或变0,说明文件做了删改或清空,回到内容末尾或0 + randomAccessFile.seek(currentLength); + return; + } + + // 读取行 + FileUtil.readLines(randomAccessFile, charset, lineHandler); + + // 记录当前读到的位置 + randomAccessFile.seek(currentLength); + } catch (IOException e) { + throw new IORuntimeException(e); + } + } +} diff --git a/src/main/java/cn/hutool/core/io/file/LineSeparator.java b/src/main/java/cn/hutool/core/io/file/LineSeparator.java new file mode 100644 index 0000000..9bb72af --- /dev/null +++ b/src/main/java/cn/hutool/core/io/file/LineSeparator.java @@ -0,0 +1,35 @@ +package cn.hutool.core.io.file; + +/** + * 换行符枚举
+ * 换行符包括: + *
+ * Mac系统换行符:"\r"
+ * Linux系统换行符:"\n"
+ * Windows系统换行符:"\r\n"
+ * 
+ * + * @see #MAC + * @see #LINUX + * @see #WINDOWS + * @author Looly + * @since 3.1.0 + */ +public enum LineSeparator { + /** Mac系统换行符:"\r" */ + MAC("\r"), + /** Linux系统换行符:"\n" */ + LINUX("\n"), + /** Windows系统换行符:"\r\n" */ + WINDOWS("\r\n"); + + private final String value; + + LineSeparator(String lineSeparator) { + this.value = lineSeparator; + } + + public String getValue() { + return this.value; + } +} diff --git a/src/main/java/cn/hutool/core/io/file/PathMover.java b/src/main/java/cn/hutool/core/io/file/PathMover.java new file mode 100644 index 0000000..5607810 --- /dev/null +++ b/src/main/java/cn/hutool/core/io/file/PathMover.java @@ -0,0 +1,166 @@ +package cn.hutool.core.io.file; + +import cn.hutool.core.io.IORuntimeException; +import cn.hutool.core.io.file.visitor.MoveVisitor; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.ObjUtil; + +import java.io.IOException; +import java.nio.file.*; + +/** + * 文件移动封装 + * + * @author looly + * @since 5.8.14 + */ +public class PathMover { + + /** + * 创建文件或目录移动器 + * + * @param src 源文件或目录 + * @param target 目标文件或目录 + * @param isOverride 是否覆盖目标文件 + * @return {@code PathMover} + */ + public static PathMover of(final Path src, final Path target, final boolean isOverride) { + return of(src, target, isOverride ? new CopyOption[]{StandardCopyOption.REPLACE_EXISTING} : new CopyOption[]{}); + } + + /** + * 创建文件或目录移动器 + * + * @param src 源文件或目录 + * @param target 目标文件或目录 + * @param options 移动参数 + * @return {@code PathMover} + */ + public static PathMover of(final Path src, final Path target, final CopyOption[] options) { + return new PathMover(src, target, options); + } + + private final Path src; + private final Path target; + private final CopyOption[] options; + + /** + * 构造 + * + * @param src 源文件或目录,不能为{@code null}且必须存在 + * @param target 目标文件或目录 + * @param options 移动参数 + */ + public PathMover(final Path src, final Path target, final CopyOption[] options) { + Assert.notNull(target, "Src path must be not null !"); + if(!PathUtil.exists(src, false)){ + throw new IllegalArgumentException("Src path is not exist!"); + } + this.src = src; + this.target = Assert.notNull(target, "Target path must be not null !"); + this.options = ObjUtil.defaultIfNull(options, new CopyOption[]{}); + } + + /** + * 移动文件或目录到目标中,例如: + *
    + *
  • 如果src和target为同一文件或目录,直接返回target。
  • + *
  • 如果src为文件,target为目录,则移动到目标目录下,存在同名文件则按照是否覆盖参数执行。
  • + *
  • 如果src为文件,target为文件,则按照是否覆盖参数执行。
  • + *
  • 如果src为文件,target为不存在的路径,则重命名源文件到目标指定的文件,如move("/a/b", "/c/d"), d不存在,则b变成d。
  • + *
  • 如果src为目录,target为文件,抛出{@link IllegalArgumentException}
  • + *
  • 如果src为目录,target为目录,则将源目录及其内容移动到目标路径目录中,如move("/a/b", "/c/d"),结果为"/c/d/b"
  • + *
  • 如果src为目录,target为不存在的路径,则重命名src到target,如move("/a/b", "/c/d"),结果为"/c/d/",相当于b重命名为d
  • + *
+ * + * @return 目标文件Path + */ + public Path move() { + final Path src = this.src; + Path target = this.target; + final CopyOption[] options = this.options; + + if (PathUtil.isDirectory(target)) { + // 创建子路径的情况,1是目标是目录,需要移动到目录下,2是目标不能存在,自动创建目录 + target = target.resolve(src.getFileName()); + } + + // issue#2893 target 不存在导致NoSuchFileException + if (Files.exists(target) && PathUtil.equals(src, target)) { + // issue#2845,当用户传入目标路径与源路径一致时,直接返回,否则会导致删除风险。 + return target; + } + + // 自动创建目标的父目录 + PathUtil.mkParentDirs(target); + try { + return Files.move(src, target, options); + } catch (final IOException e) { + if (e instanceof FileAlreadyExistsException) { + // 目标文件已存在,直接抛出异常 + // issue#I4QV0L@Gitee + throw new IORuntimeException(e); + } + // 移动失败,可能是跨分区移动导致的,采用递归移动方式 + walkMove(src, target, options); + // 移动后删除空目录 + PathUtil.del(src); + return target; + } + } + + /** + * 移动文件或目录内容到目标中,例如: + *
    + *
  • 如果src为文件,target为目录,则移动到目标目录下,存在同名文件则按照是否覆盖参数执行。
  • + *
  • 如果src为文件,target为文件,则按照是否覆盖参数执行。
  • + *
  • 如果src为文件,target为不存在的路径,则重命名源文件到目标指定的文件,如moveContent("/a/b", "/c/d"), d不存在,则b变成d。
  • + *
  • 如果src为目录,target为文件,抛出{@link IllegalArgumentException}
  • + *
  • 如果src为目录,target为目录,则将源目录下的内容移动到目标路径目录中,源目录不删除。
  • + *
  • 如果src为目录,target为不存在的路径,则创建目标路径为目录,将源目录下的内容移动到目标路径目录中,源目录不删除。
  • + *
+ * + * @return 目标文件Path + */ + public Path moveContent() { + final Path src = this.src; + if (PathUtil.isExistsAndNotDirectory(target, false)) { + // 文件移动调用move方法 + return move(); + } + + final Path target = this.target; + if (PathUtil.isExistsAndNotDirectory(target, false)) { + // 目标不能为文件 + throw new IllegalArgumentException("Can not move dir content to a file"); + } + + // issue#2893 target 不存在导致NoSuchFileException + if (PathUtil.equals(src, target)) { + // issue#2845,当用户传入目标路径与源路径一致时,直接返回,否则会导致删除风险。 + return target; + } + + final CopyOption[] options = this.options; + + // 移动失败,可能是跨分区移动导致的,采用递归移动方式 + walkMove(src, target, options); + return target; + } + + /** + * 递归移动 + * + * @param src 源目录 + * @param target 目标目录 + * @param options 移动参数 + */ + private static void walkMove(final Path src, final Path target, final CopyOption... options) { + try { + // 移动源目录下的内容而不删除目录 + Files.walkFileTree(src, new MoveVisitor(src, target, options)); + } catch (final IOException e) { + throw new IORuntimeException(e); + } + } +} diff --git a/src/main/java/cn/hutool/core/io/file/PathUtil.java b/src/main/java/cn/hutool/core/io/file/PathUtil.java new file mode 100644 index 0000000..a1f8fc2 --- /dev/null +++ b/src/main/java/cn/hutool/core/io/file/PathUtil.java @@ -0,0 +1,686 @@ +package cn.hutool.core.io.file; + +import cn.hutool.core.io.IORuntimeException; +import cn.hutool.core.io.IoUtil; +import cn.hutool.core.io.file.visitor.CopyVisitor; +import cn.hutool.core.io.file.visitor.DelVisitor; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.CharsetUtil; + +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.BufferedReader; +import java.io.File; +import java.io.FileFilter; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.charset.Charset; +import java.nio.file.AccessDeniedException; +import java.nio.file.CopyOption; +import java.nio.file.DirectoryStream; +import java.nio.file.FileVisitOption; +import java.nio.file.FileVisitResult; +import java.nio.file.FileVisitor; +import java.nio.file.Files; +import java.nio.file.LinkOption; +import java.nio.file.Path; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.StandardCopyOption; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.ArrayList; +import java.util.EnumSet; +import java.util.List; + +/** + * NIO中Path对象操作封装 + * + * @author looly + * @since 5.4.1 + */ +public class PathUtil { + /** + * 目录是否为空 + * + * @param dirPath 目录 + * @return 是否为空 + * @throws IORuntimeException IOException + */ + public static boolean isDirEmpty(Path dirPath) { + try (DirectoryStream dirStream = Files.newDirectoryStream(dirPath)) { + return !dirStream.iterator().hasNext(); + } catch (IOException e) { + throw new IORuntimeException(e); + } + } + + /** + * 递归遍历目录以及子目录中的所有文件
+ * 如果提供path为文件,直接返回过滤结果 + * + * @param path 当前遍历文件或目录 + * @param fileFilter 文件过滤规则对象,选择要保留的文件,只对文件有效,不过滤目录,null表示接收全部文件 + * @return 文件列表 + * @since 5.4.1 + */ + public static List loopFiles(Path path, FileFilter fileFilter) { + return loopFiles(path, -1, fileFilter); + } + + /** + * 递归遍历目录以及子目录中的所有文件
+ * 如果提供path为文件,直接返回过滤结果 + * + * @param path 当前遍历文件或目录 + * @param maxDepth 遍历最大深度,-1表示遍历到没有目录为止 + * @param fileFilter 文件过滤规则对象,选择要保留的文件,只对文件有效,不过滤目录,null表示接收全部文件 + * @return 文件列表 + * @since 5.4.1 + */ + public static List loopFiles(Path path, int maxDepth, FileFilter fileFilter) { + final List fileList = new ArrayList<>(); + + if (null == path || !Files.exists(path)) { + return fileList; + } else if (!isDirectory(path)) { + final File file = path.toFile(); + if (null == fileFilter || fileFilter.accept(file)) { + fileList.add(file); + } + return fileList; + } + + walkFiles(path, maxDepth, new SimpleFileVisitor() { + + @Override + public FileVisitResult visitFile(Path path, BasicFileAttributes attrs) { + final File file = path.toFile(); + if (null == fileFilter || fileFilter.accept(file)) { + fileList.add(file); + } + return FileVisitResult.CONTINUE; + } + }); + + return fileList; + } + + /** + * 遍历指定path下的文件并做处理 + * + * @param start 起始路径,必须为目录 + * @param visitor {@link FileVisitor} 接口,用于自定义在访问文件时,访问目录前后等节点做的操作 + * @see Files#walkFileTree(Path, java.util.Set, int, FileVisitor) + * @since 5.5.2 + */ + public static void walkFiles(Path start, FileVisitor visitor) { + walkFiles(start, -1, visitor); + } + + /** + * 遍历指定path下的文件并做处理 + * + * @param start 起始路径,必须为目录 + * @param maxDepth 最大遍历深度,-1表示不限制深度 + * @param visitor {@link FileVisitor} 接口,用于自定义在访问文件时,访问目录前后等节点做的操作 + * @see Files#walkFileTree(Path, java.util.Set, int, FileVisitor) + * @since 4.6.3 + */ + public static void walkFiles(Path start, int maxDepth, FileVisitor visitor) { + if (maxDepth < 0) { + // < 0 表示遍历到最底层 + maxDepth = Integer.MAX_VALUE; + } + + try { + Files.walkFileTree(start, EnumSet.noneOf(FileVisitOption.class), maxDepth, visitor); + } catch (IOException e) { + throw new IORuntimeException(e); + } + } + + /** + * 删除文件或者文件夹,不追踪软链
+ * 注意:删除文件夹时不会判断文件夹是否为空,如果不空则递归删除子文件或文件夹
+ * 某个文件删除失败会终止删除操作 + * + * @param path 文件对象 + * @return 成功与否 + * @throws IORuntimeException IO异常 + * @since 4.4.2 + */ + public static boolean del(Path path) throws IORuntimeException { + if (Files.notExists(path)) { + return true; + } + + try { + if (isDirectory(path)) { + Files.walkFileTree(path, DelVisitor.INSTANCE); + } else { + delFile(path); + } + } catch (IOException e) { + throw new IORuntimeException(e); + } + return true; + } + + /** + * 通过JDK7+的 {@link Files#copy(Path, Path, CopyOption...)} 方法拷贝文件
+ * 此方法不支持递归拷贝目录,如果src传入是目录,只会在目标目录中创建空目录 + * + * @param src 源文件路径,如果为目录只在目标中创建新目录 + * @param dest 目标文件或目录,如果为目录使用与源文件相同的文件名 + * @param options {@link StandardCopyOption} + * @return Path + * @throws IORuntimeException IO异常 + */ + public static Path copyFile(Path src, Path dest, StandardCopyOption... options) throws IORuntimeException { + return copyFile(src, dest, (CopyOption[]) options); + } + + /** + * 通过JDK7+的 {@link Files#copy(Path, Path, CopyOption...)} 方法拷贝文件
+ * 此方法不支持递归拷贝目录,如果src传入是目录,只会在目标目录中创建空目录 + * + * @param src 源文件路径,如果为目录只在目标中创建新目录 + * @param target 目标文件或目录,如果为目录使用与源文件相同的文件名 + * @param options {@link StandardCopyOption} + * @return Path + * @throws IORuntimeException IO异常 + * @since 5.4.1 + */ + public static Path copyFile(Path src, Path target, CopyOption... options) throws IORuntimeException { + Assert.notNull(src, "Source File is null !"); + Assert.notNull(target, "Destination File or directory is null !"); + + final Path targetPath = isDirectory(target) ? target.resolve(src.getFileName()) : target; + // 创建级联父目录 + mkParentDirs(targetPath); + try { + return Files.copy(src, targetPath, options); + } catch (IOException e) { + throw new IORuntimeException(e); + } + } + + /** + * 拷贝文件或目录,拷贝规则为: + * + *
    + *
  • 源文件为目录,目标也为目录或不存在,则拷贝整个目录到目标目录下
  • + *
  • 源文件为文件,目标为目录或不存在,则拷贝文件到目标目录下
  • + *
  • 源文件为文件,目标也为文件,则在{@link StandardCopyOption#REPLACE_EXISTING}情况下覆盖之
  • + *
+ * + * @param src 源文件路径,如果为目录会在目标中创建新目录 + * @param target 目标文件或目录,如果为目录使用与源文件相同的文件名 + * @param options {@link StandardCopyOption} + * @return Path + * @throws IORuntimeException IO异常 + * @since 5.5.1 + */ + public static Path copy(Path src, Path target, CopyOption... options) throws IORuntimeException { + Assert.notNull(src, "Src path must be not null !"); + Assert.notNull(target, "Target path must be not null !"); + + if (isDirectory(src)) { + return copyContent(src, target.resolve(src.getFileName()), options); + } + return copyFile(src, target, options); + } + + /** + * 拷贝目录下的所有文件或目录到目标目录中,此方法不支持文件对文件的拷贝。 + *
    + *
  • 源文件为目录,目标也为目录或不存在,则拷贝目录下所有文件和目录到目标目录下
  • + *
  • 源文件为文件,目标为目录或不存在,则拷贝文件到目标目录下
  • + *
+ * + * @param src 源文件路径,如果为目录只在目标中创建新目录 + * @param target 目标目录,如果为目录使用与源文件相同的文件名 + * @param options {@link StandardCopyOption} + * @return Path + * @throws IORuntimeException IO异常 + * @since 5.5.1 + */ + public static Path copyContent(Path src, Path target, CopyOption... options) throws IORuntimeException { + Assert.notNull(src, "Src path must be not null !"); + Assert.notNull(target, "Target path must be not null !"); + + try { + Files.walkFileTree(src, new CopyVisitor(src, target, options)); + } catch (IOException e) { + throw new IORuntimeException(e); + } + return target; + } + + /** + * 判断是否为目录,如果file为null,则返回false
+ * 此方法不会追踪到软链对应的真实地址,即软链被当作文件 + * + * @param path {@link Path} + * @return 如果为目录true + * @since 5.5.1 + */ + public static boolean isDirectory(Path path) { + return isDirectory(path, false); + } + + /** + * 判断是否为目录,如果file为null,则返回false + * + * @param path {@link Path} + * @param isFollowLinks 是否追踪到软链对应的真实地址 + * @return 如果为目录true + * @since 3.1.0 + */ + public static boolean isDirectory(Path path, boolean isFollowLinks) { + if (null == path) { + return false; + } + final LinkOption[] options = isFollowLinks ? new LinkOption[0] : new LinkOption[]{LinkOption.NOFOLLOW_LINKS}; + return Files.isDirectory(path, options); + } + + /** + * 获取指定位置的子路径部分,支持负数,例如index为-1表示从后数第一个节点位置 + * + * @param path 路径 + * @param index 路径节点位置,支持负数(负数从后向前计数) + * @return 获取的子路径 + * @since 3.1.2 + */ + public static Path getPathEle(Path path, int index) { + return subPath(path, index, index == -1 ? path.getNameCount() : index + 1); + } + + /** + * 获取指定位置的最后一个子路径部分 + * + * @param path 路径 + * @return 获取的最后一个子路径 + * @since 3.1.2 + */ + public static Path getLastPathEle(Path path) { + return getPathEle(path, path.getNameCount() - 1); + } + + /** + * 获取指定位置的子路径部分,支持负数,例如起始为-1表示从后数第一个节点位置 + * + * @param path 路径 + * @param fromIndex 起始路径节点(包括) + * @param toIndex 结束路径节点(不包括) + * @return 获取的子路径 + * @since 3.1.2 + */ + public static Path subPath(Path path, int fromIndex, int toIndex) { + if (null == path) { + return null; + } + final int len = path.getNameCount(); + + if (fromIndex < 0) { + fromIndex = len + fromIndex; + if (fromIndex < 0) { + fromIndex = 0; + } + } else if (fromIndex > len) { + fromIndex = len; + } + + if (toIndex < 0) { + toIndex = len + toIndex; + if (toIndex < 0) { + toIndex = len; + } + } else if (toIndex > len) { + toIndex = len; + } + + if (toIndex < fromIndex) { + int tmp = fromIndex; + fromIndex = toIndex; + toIndex = tmp; + } + + if (fromIndex == toIndex) { + return null; + } + return path.subpath(fromIndex, toIndex); + } + + /** + * 获取文件属性 + * + * @param path 文件路径{@link Path} + * @param isFollowLinks 是否跟踪到软链对应的真实路径 + * @return {@link BasicFileAttributes} + * @throws IORuntimeException IO异常 + */ + public static BasicFileAttributes getAttributes(Path path, boolean isFollowLinks) throws IORuntimeException { + if (null == path) { + return null; + } + + final LinkOption[] options = isFollowLinks ? new LinkOption[0] : new LinkOption[]{LinkOption.NOFOLLOW_LINKS}; + try { + return Files.readAttributes(path, BasicFileAttributes.class, options); + } catch (IOException e) { + throw new IORuntimeException(e); + } + } + + /** + * 获得输入流 + * + * @param path Path + * @return 输入流 + * @throws IORuntimeException 文件未找到 + * @since 4.0.0 + */ + public static BufferedInputStream getInputStream(Path path) throws IORuntimeException { + final InputStream in; + try { + in = Files.newInputStream(path); + } catch (IOException e) { + throw new IORuntimeException(e); + } + return IoUtil.toBuffered(in); + } + + /** + * 获得一个文件读取器 + * + * @param path 文件Path + * @return BufferedReader对象 + * @throws IORuntimeException IO异常 + * @since 4.0.0 + */ + public static BufferedReader getUtf8Reader(Path path) throws IORuntimeException { + return getReader(path, CharsetUtil.CHARSET_UTF_8); + } + + /** + * 获得一个文件读取器 + * + * @param path 文件Path + * @param charset 字符集 + * @return BufferedReader对象 + * @throws IORuntimeException IO异常 + * @since 4.0.0 + */ + public static BufferedReader getReader(Path path, Charset charset) throws IORuntimeException { + return IoUtil.getReader(getInputStream(path), charset); + } + + /** + * 读取文件的所有内容为byte数组 + * + * @param path 文件 + * @return byte数组 + * @since 5.5.4 + */ + public static byte[] readBytes(Path path) { + try { + return Files.readAllBytes(path); + } catch (IOException e) { + throw new IORuntimeException(e); + } + } + + /** + * 获得输出流 + * + * @param path Path + * @return 输入流 + * @throws IORuntimeException 文件未找到 + * @since 5.4.1 + */ + public static BufferedOutputStream getOutputStream(Path path) throws IORuntimeException { + final OutputStream in; + try { + in = Files.newOutputStream(path); + } catch (IOException e) { + throw new IORuntimeException(e); + } + return IoUtil.toBuffered(in); + } + + /** + * 修改文件或目录的文件名,不变更路径,只是简单修改文件名
+ * + *
+	 * FileUtil.rename(file, "aaa.jpg", false) xx/xx.png =》xx/aaa.jpg
+	 * 
+ * + * @param path 被修改的文件 + * @param newName 新的文件名,包括扩展名 + * @param isOverride 是否覆盖目标文件 + * @return 目标文件Path + * @since 5.4.1 + */ + public static Path rename(Path path, String newName, boolean isOverride) { + return move(path, path.resolveSibling(newName), isOverride); + } + + /** + * 移动文件或目录到目标中,例如: + *
    + *
  • 如果src和target为同一文件或目录,直接返回target。
  • + *
  • 如果src为文件,target为目录,则移动到目标目录下,存在同名文件则按照是否覆盖参数执行。
  • + *
  • 如果src为文件,target为文件,则按照是否覆盖参数执行。
  • + *
  • 如果src为文件,target为不存在的路径,则重命名源文件到目标指定的文件,如moveContent("/a/b", "/c/d"), d不存在,则b变成d。
  • + *
  • 如果src为目录,target为文件,抛出{@link IllegalArgumentException}
  • + *
  • 如果src为目录,target为目录,则将源目录及其内容移动到目标路径目录中,如move("/a/b", "/c/d"),结果为"/c/d/b"
  • + *
  • 如果src为目录,target为不存在的路径,则重命名src到target,如move("/a/b", "/c/d"),结果为"/c/d/",相当于b重命名为d
  • + *
+ * + * @param src 源文件或目录路径 + * @param target 目标路径,如果为目录,则移动到此目录下 + * @param isOverride 是否覆盖目标文件 + * @return 目标文件Path + */ + public static Path move(Path src, Path target, boolean isOverride) { + return PathMover.of(src, target, isOverride).move(); + } + + /** + * 移动文件或目录内容到目标中,例如: + *
    + *
  • 如果src为文件,target为目录,则移动到目标目录下,存在同名文件则按照是否覆盖参数执行。
  • + *
  • 如果src为文件,target为文件,则按照是否覆盖参数执行。
  • + *
  • 如果src为文件,target为不存在的路径,则重命名源文件到目标指定的文件,如moveContent("/a/b", "/c/d"), d不存在,则b变成d。
  • + *
  • 如果src为目录,target为文件,抛出{@link IllegalArgumentException}
  • + *
  • 如果src为目录,target为目录,则将源目录下的内容移动到目标路径目录中,源目录不删除。
  • + *
  • 如果src为目录,target为不存在的路径,则创建目标路径为目录,将源目录下的内容移动到目标路径目录中,源目录不删除。
  • + *
+ * + * @param src 源文件或目录路径 + * @param target 目标路径,如果为目录,则移动到此目录下 + * @param isOverride 是否覆盖目标文件 + * @return 目标文件Path + */ + public static Path moveContent(Path src, Path target, boolean isOverride) { + return PathMover.of(src, target, isOverride).moveContent(); + } + + /** + * 检查两个文件是否是同一个文件
+ * 所谓文件相同,是指Path对象是否指向同一个文件或文件夹 + * + * @param file1 文件1 + * @param file2 文件2 + * @return 是否相同 + * @throws IORuntimeException IO异常 + * @see Files#isSameFile(Path, Path) + * @since 5.4.1 + */ + public static boolean equals(Path file1, Path file2) throws IORuntimeException { + try { + return Files.isSameFile(file1, file2); + } catch (IOException e) { + throw new IORuntimeException(e); + } + } + + /** + * 判断是否为文件,如果file为null,则返回false + * + * @param path 文件 + * @param isFollowLinks 是否跟踪软链(快捷方式) + * @return 如果为文件true + * @see Files#isRegularFile(Path, LinkOption...) + */ + public static boolean isFile(Path path, boolean isFollowLinks) { + if (null == path) { + return false; + } + final LinkOption[] options = isFollowLinks ? new LinkOption[0] : new LinkOption[]{LinkOption.NOFOLLOW_LINKS}; + return Files.isRegularFile(path, options); + } + + /** + * 判断是否为符号链接文件 + * + * @param path 被检查的文件 + * @return 是否为符号链接文件 + * @since 4.4.2 + */ + public static boolean isSymlink(Path path) { + return Files.isSymbolicLink(path); + } + + /** + * 判断文件或目录是否存在 + * + * @param path 文件 + * @param isFollowLinks 是否跟踪软链(快捷方式) + * @return 是否存在 + * @since 5.5.3 + */ + public static boolean exists(Path path, boolean isFollowLinks) { + final LinkOption[] options = isFollowLinks ? new LinkOption[0] : new LinkOption[]{LinkOption.NOFOLLOW_LINKS}; + return Files.exists(path, options); + } + + /** + * 判断是否存在且为非目录 + *
    + *
  • 如果path为{@code null},返回{@code false}
  • + *
  • 如果path不存在,返回{@code false}
  • + *
+ * + * @param path {@link Path} + * @param isFollowLinks 是否追踪到软链对应的真实地址 + * @return 如果为目录true + * @since 5.8.14 + */ + public static boolean isExistsAndNotDirectory(final Path path, final boolean isFollowLinks) { + return exists(path, isFollowLinks) && !isDirectory(path, isFollowLinks); + } + + /** + * 判断给定的目录是否为给定文件或文件夹的子目录 + * + * @param parent 父目录 + * @param sub 子目录 + * @return 子目录是否为父目录的子目录 + * @since 5.5.5 + */ + public static boolean isSub(Path parent, Path sub) { + return toAbsNormal(sub).startsWith(toAbsNormal(parent)); + } + + /** + * 将Path路径转换为标准的绝对路径 + * + * @param path 文件或目录Path + * @return 转换后的Path + * @since 5.5.5 + */ + public static Path toAbsNormal(Path path) { + Assert.notNull(path); + return path.toAbsolutePath().normalize(); + } + + /** + * 获得文件的MimeType + * + * @param file 文件 + * @return MimeType + * @see Files#probeContentType(Path) + * @since 5.5.5 + */ + public static String getMimeType(Path file) { + try { + return Files.probeContentType(file); + } catch (IOException e) { + throw new IORuntimeException(e); + } + } + + /** + * 创建所给目录及其父目录 + * + * @param dir 目录 + * @return 目录 + * @since 5.5.7 + */ + public static Path mkdir(Path dir) { + if (null != dir && !exists(dir, false)) { + try { + Files.createDirectories(dir); + } catch (IOException e) { + throw new IORuntimeException(e); + } + } + return dir; + } + + /** + * 创建所给文件或目录的父目录 + * + * @param path 文件或目录 + * @return 父目录 + * @since 5.5.7 + */ + public static Path mkParentDirs(Path path) { + return mkdir(path.getParent()); + } + + /** + * 获取{@link Path}文件名 + * + * @param path {@link Path} + * @return 文件名 + * @since 5.7.15 + */ + public static String getName(Path path) { + if (null == path) { + return null; + } + return path.getFileName().toString(); + } + + /** + * 删除文件或空目录,不追踪软链 + * + * @param path 文件对象 + * @throws IOException IO异常 + * @since 5.7.7 + */ + protected static void delFile(Path path) throws IOException { + try { + Files.delete(path); + } catch (AccessDeniedException e) { + // 可能遇到只读文件,无法删除.使用 file 方法删除 + if (!path.toFile().delete()) { + throw e; + } + } + } +} diff --git a/src/main/java/cn/hutool/core/io/file/Tailer.java b/src/main/java/cn/hutool/core/io/file/Tailer.java new file mode 100644 index 0000000..de3ddc9 --- /dev/null +++ b/src/main/java/cn/hutool/core/io/file/Tailer.java @@ -0,0 +1,236 @@ +package cn.hutool.core.io.file; + +import cn.hutool.core.date.DateUnit; +import cn.hutool.core.exceptions.UtilException; +import cn.hutool.core.io.FileUtil; +import cn.hutool.core.io.IORuntimeException; +import cn.hutool.core.io.IoUtil; +import cn.hutool.core.io.LineHandler; +import cn.hutool.core.lang.Console; +import cn.hutool.core.util.CharUtil; +import cn.hutool.core.util.CharsetUtil; + +import java.io.File; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.io.Serializable; +import java.nio.charset.Charset; +import java.util.Stack; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +/** + * 文件内容跟随器,实现类似Linux下"tail -f"命令功能 + * + * @author looly + * @since 4.5.2 + */ +public class Tailer implements Serializable { + private static final long serialVersionUID = 1L; + + public static final LineHandler CONSOLE_HANDLER = new ConsoleLineHandler(); + + /** 编码 */ + private final Charset charset; + /** 行处理器 */ + private final LineHandler lineHandler; + /** 初始读取的行数 */ + private final int initReadLine; + /** 定时任务检查间隔时长 */ + private final long period; + + private final RandomAccessFile randomAccessFile; + private final ScheduledExecutorService executorService; + + /** + * 构造,默认UTF-8编码 + * + * @param file 文件 + * @param lineHandler 行处理器 + */ + public Tailer(File file, LineHandler lineHandler) { + this(file, lineHandler, 0); + } + + /** + * 构造,默认UTF-8编码 + * + * @param file 文件 + * @param lineHandler 行处理器 + * @param initReadLine 启动时预读取的行数 + */ + public Tailer(File file, LineHandler lineHandler, int initReadLine) { + this(file, CharsetUtil.CHARSET_UTF_8, lineHandler, initReadLine, DateUnit.SECOND.getMillis()); + } + + /** + * 构造 + * + * @param file 文件 + * @param charset 编码 + * @param lineHandler 行处理器 + */ + public Tailer(File file, Charset charset, LineHandler lineHandler) { + this(file, charset, lineHandler, 0, DateUnit.SECOND.getMillis()); + } + + /** + * 构造 + * + * @param file 文件 + * @param charset 编码 + * @param lineHandler 行处理器 + * @param initReadLine 启动时预读取的行数 + * @param period 检查间隔 + */ + public Tailer(File file, Charset charset, LineHandler lineHandler, int initReadLine, long period) { + checkFile(file); + this.charset = charset; + this.lineHandler = lineHandler; + this.period = period; + this.initReadLine = initReadLine; + this.randomAccessFile = FileUtil.createRandomAccessFile(file, FileMode.r); + this.executorService = Executors.newSingleThreadScheduledExecutor(); + } + + /** + * 开始监听 + */ + public void start() { + start(false); + } + + /** + * 开始监听 + * + * @param async 是否异步执行 + */ + public void start(boolean async) { + // 初始读取 + try { + this.readTail(); + } catch (IOException e) { + throw new IORuntimeException(e); + } + + final LineReadWatcher lineReadWatcher = new LineReadWatcher(this.randomAccessFile, this.charset, this.lineHandler); + final ScheduledFuture scheduledFuture = this.executorService.scheduleAtFixedRate(// + lineReadWatcher, // + 0, // + this.period, TimeUnit.MILLISECONDS// + ); + + if (!async) { + try { + scheduledFuture.get(); + } catch (ExecutionException e) { + throw new UtilException(e); + } catch (InterruptedException e) { + // ignore and exist + } + } + } + + /** + * 结束,此方法需在异步模式或 + */ + public void stop(){ + try{ + this.executorService.shutdown(); + }finally { + IoUtil.close(this.randomAccessFile); + } + } + + // ---------------------------------------------------------------------------------------- Private method start + /** + * 预读取行 + * + * @throws IOException IO异常 + */ + private void readTail() throws IOException { + final long len = this.randomAccessFile.length(); + + if (initReadLine > 0) { + Stack stack = new Stack<>(); + + long start = this.randomAccessFile.getFilePointer(); + long nextEnd = (len - 1) < 0 ? 0 : len - 1; + this.randomAccessFile.seek(nextEnd); + int c; + int currentLine = 0; + while (nextEnd > start) { + // 满 + if (currentLine > initReadLine) { + break; + } + + c = this.randomAccessFile.read(); + if (c == CharUtil.LF || c == CharUtil.CR) { + // FileUtil.readLine(this.randomAccessFile, this.charset, this.lineHandler); + final String line = FileUtil.readLine(this.randomAccessFile, this.charset); + if(null != line) { + stack.push(line); + } + currentLine++; + nextEnd--; + } + nextEnd--; + this.randomAccessFile.seek(nextEnd); + if (nextEnd == 0) { + // 当文件指针退至文件开始处,输出第一行 + // FileUtil.readLine(this.randomAccessFile, this.charset, this.lineHandler); + final String line = FileUtil.readLine(this.randomAccessFile, this.charset); + if(null != line) { + stack.push(line); + } + break; + } + } + + // 输出缓存栈中的内容 + while (!stack.isEmpty()) { + this.lineHandler.handle(stack.pop()); + } + } + + // 将指针置于末尾 + try { + this.randomAccessFile.seek(len); + } catch (IOException e) { + throw new IORuntimeException(e); + } + } + + /** + * 检查文件有效性 + * + * @param file 文件 + */ + private static void checkFile(File file) { + if (!file.exists()) { + throw new UtilException("File [{}] not exist !", file.getAbsolutePath()); + } + if (!file.isFile()) { + throw new UtilException("Path [{}] is not a file !", file.getAbsolutePath()); + } + } + // ---------------------------------------------------------------------------------------- Private method end + + /** + * 命令行打印的行处理器 + * + * @author looly + * @since 4.5.2 + */ + public static class ConsoleLineHandler implements LineHandler { + @Override + public void handle(String line) { + Console.log(line); + } + } + +} diff --git a/src/main/java/cn/hutool/core/io/file/package-info.java b/src/main/java/cn/hutool/core/io/file/package-info.java new file mode 100644 index 0000000..53c289f --- /dev/null +++ b/src/main/java/cn/hutool/core/io/file/package-info.java @@ -0,0 +1,7 @@ +/** + * 对文件读写的封装,包括文件拷贝、文件读取、文件写出、行处理等 + * + * @author looly + * + */ +package cn.hutool.core.io.file; \ No newline at end of file diff --git a/src/main/java/cn/hutool/core/io/file/visitor/CopyVisitor.java b/src/main/java/cn/hutool/core/io/file/visitor/CopyVisitor.java new file mode 100644 index 0000000..840f9c3 --- /dev/null +++ b/src/main/java/cn/hutool/core/io/file/visitor/CopyVisitor.java @@ -0,0 +1,103 @@ +package cn.hutool.core.io.file.visitor; + +import cn.hutool.core.io.file.PathUtil; + +import java.io.IOException; +import java.nio.file.CopyOption; +import java.nio.file.FileAlreadyExistsException; +import java.nio.file.FileVisitResult; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.attribute.BasicFileAttributes; + +/** + * 文件拷贝的FileVisitor实现,用于递归遍历拷贝目录,此类非线程安全
+ * 此类在遍历源目录并复制过程中会自动创建目标目录中不存在的上级目录。 + * + * @author looly + * @since 5.5.1 + */ +public class CopyVisitor extends SimpleFileVisitor { + + /** + * 源Path,或基准路径,用于计算被拷贝文件的相对路径 + */ + private final Path source; + private final Path target; + private final CopyOption[] copyOptions; + + /** + * 标记目标目录是否创建,省略每次判断目标是否存在 + */ + private boolean isTargetCreated; + + /** + * 构造 + * + * @param source 源Path,或基准路径,用于计算被拷贝文件的相对路径 + * @param target 目标Path + * @param copyOptions 拷贝选项,如跳过已存在等 + */ + public CopyVisitor(Path source, Path target, CopyOption... copyOptions) { + if (PathUtil.exists(target, false) && !PathUtil.isDirectory(target)) { + throw new IllegalArgumentException("Target must be a directory"); + } + this.source = source; + this.target = target; + this.copyOptions = copyOptions; + } + + @Override + public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException { + initTargetDir(); + // 将当前目录相对于源路径转换为相对于目标路径 + final Path targetDir = resolveTarget(dir); + + // 在目录不存在的情况下,copy方法会创建新目录 + try { + Files.copy(dir, targetDir, copyOptions); + } catch (FileAlreadyExistsException e) { + if (!Files.isDirectory(targetDir)) { + // 目标文件存在抛出异常,目录忽略 + throw e; + } + } + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) + throws IOException { + initTargetDir(); + // 如果目标存在,无论目录还是文件都抛出FileAlreadyExistsException异常,此处不做特别处理 + Files.copy(file, resolveTarget(file), copyOptions); + return FileVisitResult.CONTINUE; + } + + /** + * 根据源文件或目录路径,拼接生成目标的文件或目录路径
+ * 原理是首先截取源路径,得到相对路径,再和目标路径拼接 + * + *

+ * 如:源路径是 /opt/test/,需要拷贝的文件是 /opt/test/a/a.txt,得到相对路径 a/a.txt
+ * 目标路径是/home/,则得到最终目标路径是 /home/a/a.txt + *

+ * + * @param file 需要拷贝的文件或目录Path + * @return 目标Path + */ + private Path resolveTarget(Path file) { + return target.resolve(source.relativize(file)); + } + + /** + * 初始化目标文件或目录 + */ + private void initTargetDir() { + if (!this.isTargetCreated) { + PathUtil.mkdir(this.target); + this.isTargetCreated = true; + } + } +} diff --git a/src/main/java/cn/hutool/core/io/file/visitor/DelVisitor.java b/src/main/java/cn/hutool/core/io/file/visitor/DelVisitor.java new file mode 100644 index 0000000..4e435fb --- /dev/null +++ b/src/main/java/cn/hutool/core/io/file/visitor/DelVisitor.java @@ -0,0 +1,44 @@ +package cn.hutool.core.io.file.visitor; + +import java.io.IOException; +import java.nio.file.FileVisitResult; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.attribute.BasicFileAttributes; + +/** + * 删除操作的FileVisitor实现,用于递归遍历删除文件夹 + * + * @author looly + * @since 5.5.1 + */ +public class DelVisitor extends SimpleFileVisitor { + + public static DelVisitor INSTANCE = new DelVisitor(); + + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { + Files.delete(file); + return FileVisitResult.CONTINUE; + } + + /** + * 访问目录结束后删除目录,当执行此方法时,子文件或目录都已访问(删除)完毕
+ * 理论上当执行到此方法时,目录下已经被清空了 + * + * @param dir 目录 + * @param e 异常 + * @return {@link FileVisitResult} + * @throws IOException IO异常 + */ + @Override + public FileVisitResult postVisitDirectory(Path dir, IOException e) throws IOException { + if (e == null) { + Files.delete(dir); + return FileVisitResult.CONTINUE; + } else { + throw e; + } + } +} diff --git a/src/main/java/cn/hutool/core/io/file/visitor/MoveVisitor.java b/src/main/java/cn/hutool/core/io/file/visitor/MoveVisitor.java new file mode 100644 index 0000000..926ad96 --- /dev/null +++ b/src/main/java/cn/hutool/core/io/file/visitor/MoveVisitor.java @@ -0,0 +1,75 @@ +package cn.hutool.core.io.file.visitor; + +import cn.hutool.core.io.file.PathUtil; + +import java.io.IOException; +import java.nio.file.CopyOption; +import java.nio.file.FileAlreadyExistsException; +import java.nio.file.FileVisitResult; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.attribute.BasicFileAttributes; + +/** + * 文件移动操作的FileVisitor实现,用于递归遍历移动目录和文件,此类非线程安全
+ * 此类在遍历源目录并移动过程中会自动创建目标目录中不存在的上级目录。 + * + * @author looly + * @since 5.7.7 + */ +public class MoveVisitor extends SimpleFileVisitor { + + private final Path source; + private final Path target; + private boolean isTargetCreated; + private final CopyOption[] copyOptions; + + /** + * 构造 + * + * @param source 源Path + * @param target 目标Path + * @param copyOptions 拷贝(移动)选项 + */ + public MoveVisitor(Path source, Path target, CopyOption... copyOptions) { + if(PathUtil.exists(target, false) && !PathUtil.isDirectory(target)){ + throw new IllegalArgumentException("Target must be a directory"); + } + this.source = source; + this.target = target; + this.copyOptions = copyOptions; + } + + @Override + public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) + throws IOException { + initTarget(); + // 将当前目录相对于源路径转换为相对于目标路径 + final Path targetDir = target.resolve(source.relativize(dir)); + if(!Files.exists(targetDir)){ + Files.createDirectories(targetDir); + } else if(!Files.isDirectory(targetDir)){ + throw new FileAlreadyExistsException(targetDir.toString()); + } + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) + throws IOException { + initTarget(); + Files.move(file, target.resolve(source.relativize(file)), copyOptions); + return FileVisitResult.CONTINUE; + } + + /** + * 初始化目标文件或目录 + */ + private void initTarget(){ + if(!this.isTargetCreated){ + PathUtil.mkdir(this.target); + this.isTargetCreated = true; + } + } +} diff --git a/src/main/java/cn/hutool/core/io/file/visitor/package-info.java b/src/main/java/cn/hutool/core/io/file/visitor/package-info.java new file mode 100644 index 0000000..d58d049 --- /dev/null +++ b/src/main/java/cn/hutool/core/io/file/visitor/package-info.java @@ -0,0 +1,7 @@ +/** + * FileVisitor功能性实现,包括递归删除、拷贝等 + * + * @author looly + * + */ +package cn.hutool.core.io.file.visitor; \ No newline at end of file diff --git a/src/main/java/cn/hutool/core/io/package-info.java b/src/main/java/cn/hutool/core/io/package-info.java new file mode 100644 index 0000000..13ff868 --- /dev/null +++ b/src/main/java/cn/hutool/core/io/package-info.java @@ -0,0 +1,7 @@ +/** + * IO相关封装和工具类,包括Inputstream和OutputStream实现类,工具包括流工具IoUtil、文件工具FileUtil和Buffer工具BufferUtil + * + * @author looly + * + */ +package cn.hutool.core.io; \ No newline at end of file diff --git a/src/main/java/cn/hutool/core/io/resource/BytesResource.java b/src/main/java/cn/hutool/core/io/resource/BytesResource.java new file mode 100644 index 0000000..91c1041 --- /dev/null +++ b/src/main/java/cn/hutool/core/io/resource/BytesResource.java @@ -0,0 +1,70 @@ +package cn.hutool.core.io.resource; + +import cn.hutool.core.io.IORuntimeException; +import cn.hutool.core.util.StrUtil; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.io.Serializable; +import java.net.URL; +import java.nio.charset.Charset; + +/** + * 基于byte[]的资源获取器
+ * 注意:此对象中getUrl方法始终返回null + * + * @author looly + * @since 4.0.9 + */ +public class BytesResource implements Resource, Serializable { + private static final long serialVersionUID = 1L; + + private final byte[] bytes; + private final String name; + + /** + * 构造 + * + * @param bytes 字节数组 + */ + public BytesResource(byte[] bytes) { + this(bytes, null); + } + + /** + * 构造 + * + * @param bytes 字节数组 + * @param name 资源名称 + */ + public BytesResource(byte[] bytes, String name) { + this.bytes = bytes; + this.name = name; + } + + @Override + public String getName() { + return this.name; + } + + @Override + public URL getUrl() { + return null; + } + + @Override + public InputStream getStream() { + return new ByteArrayInputStream(this.bytes); + } + + @Override + public String readStr(Charset charset) throws IORuntimeException { + return StrUtil.str(this.bytes, charset); + } + + @Override + public byte[] readBytes() throws IORuntimeException { + return this.bytes; + } + +} diff --git a/src/main/java/cn/hutool/core/io/resource/CharSequenceResource.java b/src/main/java/cn/hutool/core/io/resource/CharSequenceResource.java new file mode 100644 index 0000000..a6950d9 --- /dev/null +++ b/src/main/java/cn/hutool/core/io/resource/CharSequenceResource.java @@ -0,0 +1,91 @@ +package cn.hutool.core.io.resource; + +import cn.hutool.core.io.IORuntimeException; +import cn.hutool.core.io.IoUtil; +import cn.hutool.core.util.CharsetUtil; +import cn.hutool.core.util.StrUtil; + +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.io.Serializable; +import java.io.StringReader; +import java.net.URL; +import java.nio.charset.Charset; + +/** + * {@link CharSequence}资源,字符串做为资源 + * + * @author looly + * @since 5.5.2 + */ +public class CharSequenceResource implements Resource, Serializable { + private static final long serialVersionUID = 1L; + + private final CharSequence data; + private final CharSequence name; + private final Charset charset; + + /** + * 构造,使用UTF8编码 + * + * @param data 资源数据 + */ + public CharSequenceResource(CharSequence data) { + this(data, null); + } + + /** + * 构造,使用UTF8编码 + * + * @param data 资源数据 + * @param name 资源名称 + */ + public CharSequenceResource(CharSequence data, String name) { + this(data, name, CharsetUtil.CHARSET_UTF_8); + } + + /** + * 构造 + * + * @param data 资源数据 + * @param name 资源名称 + * @param charset 编码 + */ + public CharSequenceResource(CharSequence data, CharSequence name, Charset charset) { + this.data = data; + this.name = name; + this.charset = charset; + } + + @Override + public String getName() { + return StrUtil.str(this.name); + } + + @Override + public URL getUrl() { + return null; + } + + @Override + public InputStream getStream() { + return new ByteArrayInputStream(readBytes()); + } + + @Override + public BufferedReader getReader(Charset charset) { + return IoUtil.getReader(new StringReader(this.data.toString())); + } + + @Override + public String readStr(Charset charset) throws IORuntimeException { + return this.data.toString(); + } + + @Override + public byte[] readBytes() throws IORuntimeException { + return this.data.toString().getBytes(this.charset); + } + +} diff --git a/src/main/java/cn/hutool/core/io/resource/ClassPathResource.java b/src/main/java/cn/hutool/core/io/resource/ClassPathResource.java new file mode 100644 index 0000000..96af500 --- /dev/null +++ b/src/main/java/cn/hutool/core/io/resource/ClassPathResource.java @@ -0,0 +1,145 @@ +package cn.hutool.core.io.resource; + +import cn.hutool.core.io.FileUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.ClassUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.core.util.URLUtil; + +import java.net.URL; + +/** + * ClassPath单一资源访问类
+ * 传入路径path必须为相对路径,如果传入绝对路径,Linux路径会去掉开头的“/”,而Windows路径会直接报错。
+ * 传入的path所指向的资源必须存在,否则报错 + * + * @author Looly + * + */ +public class ClassPathResource extends UrlResource { + private static final long serialVersionUID = 1L; + + private final String path; + private final ClassLoader classLoader; + private final Class clazz; + + // -------------------------------------------------------------------------------------- Constructor start + /** + * 构造 + * + * @param path 相对于ClassPath的路径 + */ + public ClassPathResource(String path) { + this(path, null, null); + } + + /** + * 构造 + * + * @param path 相对于ClassPath的路径 + * @param classLoader {@link ClassLoader} + */ + public ClassPathResource(String path, ClassLoader classLoader) { + this(path, classLoader, null); + } + + /** + * 构造 + * + * @param path 相对于给定Class的路径 + * @param clazz {@link Class} 用于定位路径 + */ + public ClassPathResource(String path, Class clazz) { + this(path, null, clazz); + } + + /** + * 构造 + * + * @param pathBaseClassLoader 相对路径 + * @param classLoader {@link ClassLoader} + * @param clazz {@link Class} 用于定位路径 + */ + public ClassPathResource(String pathBaseClassLoader, ClassLoader classLoader, Class clazz) { + super((URL) null); + Assert.notNull(pathBaseClassLoader, "Path must not be null"); + + final String path = normalizePath(pathBaseClassLoader); + this.path = path; + this.name = StrUtil.isBlank(path) ? null : FileUtil.getName(path); + + this.classLoader = ObjectUtil.defaultIfNull(classLoader, ClassUtil::getClassLoader); + this.clazz = clazz; + initUrl(); + } + // -------------------------------------------------------------------------------------- Constructor end + + /** + * 获得Path + * + * @return path + */ + public final String getPath() { + return this.path; + } + + /** + * 获得绝对路径Path
+ * 对于不存在的资源,返回拼接后的绝对路径 + * + * @return 绝对路径path + */ + public final String getAbsolutePath() { + if (FileUtil.isAbsolutePath(this.path)) { + return this.path; + } + // url在初始化的时候已经断言,此处始终不为null + return FileUtil.normalize(URLUtil.getDecodedPath(this.url)); + } + + /** + * 获得 {@link ClassLoader} + * + * @return {@link ClassLoader} + */ + public final ClassLoader getClassLoader() { + return this.classLoader; + } + + /** + * 根据给定资源初始化URL + */ + private void initUrl() { + if (null != this.clazz) { + super.url = this.clazz.getResource(this.path); + } else if (null != this.classLoader) { + super.url = this.classLoader.getResource(this.path); + } else { + super.url = ClassLoader.getSystemResource(this.path); + } + if (null == super.url) { + throw new NoResourceException("Resource of path [{}] not exist!", this.path); + } + } + + @Override + public String toString() { + return (null == this.path) ? super.toString() : "classpath:" + this.path; + } + + /** + * 标准化Path格式 + * + * @param path Path + * @return 标准化后的path + */ + private String normalizePath(String path) { + // 标准化路径 + path = FileUtil.normalize(path); + path = StrUtil.removePrefix(path, StrUtil.SLASH); + + Assert.isFalse(FileUtil.isAbsolutePath(path), "Path [{}] must be a relative path !", path); + return path; + } +} diff --git a/src/main/java/cn/hutool/core/io/resource/FileResource.java b/src/main/java/cn/hutool/core/io/resource/FileResource.java new file mode 100644 index 0000000..3fcdea2 --- /dev/null +++ b/src/main/java/cn/hutool/core/io/resource/FileResource.java @@ -0,0 +1,107 @@ +package cn.hutool.core.io.resource; + +import cn.hutool.core.io.FileUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.URLUtil; + +import java.io.File; +import java.io.InputStream; +import java.io.Serializable; +import java.net.URL; +import java.nio.file.Path; + +/** + * 文件资源访问对象,支持{@link Path} 和 {@link File} 访问 + * + * @author looly + */ +public class FileResource implements Resource, Serializable { + private static final long serialVersionUID = 1L; + + private final File file; + private final long lastModified; + private final String name; + + // ----------------------------------------------------------------------- Constructor start + /** + * 构造 + * + * @param path 文件绝对路径或相对ClassPath路径,但是这个路径不能指向一个jar包中的文件 + */ + public FileResource(String path) { + this(FileUtil.file(path)); + } + + /** + * 构造,文件名使用文件本身的名字,带扩展名 + * + * @param path 文件 + * @since 4.4.1 + */ + public FileResource(Path path) { + this(path.toFile()); + } + + /** + * 构造,文件名使用文件本身的名字,带扩展名 + * + * @param file 文件 + */ + public FileResource(File file) { + this(file, null); + } + + /** + * 构造 + * + * @param file 文件 + * @param fileName 文件名,带扩展名,如果为null获取文件本身的文件名 + */ + public FileResource(File file, String fileName) { + Assert.notNull(file, "File must be not null !"); + this.file = file; + this.lastModified = file.lastModified(); + this.name = ObjectUtil.defaultIfNull(fileName, file::getName); + } + + // ----------------------------------------------------------------------- Constructor end + + @Override + public String getName() { + return this.name; + } + + @Override + public URL getUrl(){ + return URLUtil.getURL(this.file); + } + + @Override + public InputStream getStream() throws NoResourceException { + return FileUtil.getInputStream(this.file); + } + + /** + * 获取文件 + * + * @return 文件 + */ + public File getFile() { + return this.file; + } + + @Override + public boolean isModified() { + return this.lastModified != file.lastModified(); + } + + /** + * 返回路径 + * @return 返回URL路径 + */ + @Override + public String toString() { + return this.file.toString(); + } +} diff --git a/src/main/java/cn/hutool/core/io/resource/InputStreamResource.java b/src/main/java/cn/hutool/core/io/resource/InputStreamResource.java new file mode 100644 index 0000000..887bc76 --- /dev/null +++ b/src/main/java/cn/hutool/core/io/resource/InputStreamResource.java @@ -0,0 +1,54 @@ +package cn.hutool.core.io.resource; + +import java.io.InputStream; +import java.io.Serializable; +import java.net.URL; + +/** + * 基于{@link InputStream}的资源获取器
+ * 注意:此对象中getUrl方法始终返回null + * + * @author looly + * @since 4.0.9 + */ +public class InputStreamResource implements Resource, Serializable { + private static final long serialVersionUID = 1L; + + private final InputStream in; + private final String name; + + /** + * 构造 + * + * @param in {@link InputStream} + */ + public InputStreamResource(InputStream in) { + this(in, null); + } + + /** + * 构造 + * + * @param in {@link InputStream} + * @param name 资源名称 + */ + public InputStreamResource(InputStream in, String name) { + this.in = in; + this.name = name; + } + + @Override + public String getName() { + return this.name; + } + + @Override + public URL getUrl() { + return null; + } + + @Override + public InputStream getStream() { + return this.in; + } +} diff --git a/src/main/java/cn/hutool/core/io/resource/MultiFileResource.java b/src/main/java/cn/hutool/core/io/resource/MultiFileResource.java new file mode 100644 index 0000000..4abe22c --- /dev/null +++ b/src/main/java/cn/hutool/core/io/resource/MultiFileResource.java @@ -0,0 +1,64 @@ +package cn.hutool.core.io.resource; + +import java.io.File; +import java.util.Collection; + +/** + * 多文件组合资源
+ * 此资源为一个利用游标自循环资源,只有调用{@link #next()} 方法才会获取下一个资源,使用完毕后调用{@link #reset()}方法重置游标 + * + * @author looly + * + */ +public class MultiFileResource extends MultiResource{ + private static final long serialVersionUID = 1L; + + /** + * 构造 + * + * @param files 文件资源列表 + */ + public MultiFileResource(Collection files) { + add(files); + } + + /** + * 构造 + * + * @param files 文件资源列表 + */ + public MultiFileResource(File... files) { + add(files); + } + + /** + * 增加文件资源 + * + * @param files 文件资源 + * @return this + */ + public MultiFileResource add(File... files) { + for (File file : files) { + this.add(new FileResource(file)); + } + return this; + } + + /** + * 增加文件资源 + * + * @param files 文件资源 + * @return this + */ + public MultiFileResource add(Collection files) { + for (File file : files) { + this.add(new FileResource(file)); + } + return this; + } + + @Override + public MultiFileResource add(Resource resource) { + return (MultiFileResource)super.add(resource); + } +} diff --git a/src/main/java/cn/hutool/core/io/resource/MultiResource.java b/src/main/java/cn/hutool/core/io/resource/MultiResource.java new file mode 100644 index 0000000..608265b --- /dev/null +++ b/src/main/java/cn/hutool/core/io/resource/MultiResource.java @@ -0,0 +1,132 @@ +package cn.hutool.core.io.resource; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.io.IORuntimeException; + +import java.io.BufferedReader; +import java.io.InputStream; +import java.io.Serializable; +import java.net.URL; +import java.nio.charset.Charset; +import java.util.Collection; +import java.util.ConcurrentModificationException; +import java.util.Iterator; +import java.util.List; + +/** + * 多资源组合资源
+ * 此资源为一个利用游标自循环资源,只有调用{@link #next()} 方法才会获取下一个资源,使用完毕后调用{@link #reset()}方法重置游标 + * + * @author looly + * @since 4.1.0 + */ +public class MultiResource implements Resource, Iterable, Iterator, Serializable { + private static final long serialVersionUID = 1L; + + private final List resources; + private int cursor; + + /** + * 构造 + * + * @param resources 资源数组 + */ + public MultiResource(Resource... resources) { + this(CollUtil.newArrayList(resources)); + } + + /** + * 构造 + * + * @param resources 资源列表 + */ + public MultiResource(Collection resources) { + if(resources instanceof List) { + this.resources = (List)resources; + }else { + this.resources = CollUtil.newArrayList(resources); + } + } + + @Override + public String getName() { + return resources.get(cursor).getName(); + } + + @Override + public URL getUrl() { + return resources.get(cursor).getUrl(); + } + + @Override + public InputStream getStream() { + return resources.get(cursor).getStream(); + } + + @Override + public boolean isModified() { + return resources.get(cursor).isModified(); + } + + @Override + public BufferedReader getReader(Charset charset) { + return resources.get(cursor).getReader(charset); + } + + @Override + public String readStr(Charset charset) throws IORuntimeException { + return resources.get(cursor).readStr(charset); + } + + @Override + public String readUtf8Str() throws IORuntimeException { + return resources.get(cursor).readUtf8Str(); + } + + @Override + public byte[] readBytes() throws IORuntimeException { + return resources.get(cursor).readBytes(); + } + + @Override + public Iterator iterator() { + return resources.iterator(); + } + + @Override + public boolean hasNext() { + return cursor < resources.size(); + } + + @Override + public synchronized Resource next() { + if (cursor >= resources.size()) { + throw new ConcurrentModificationException(); + } + this.cursor++; + return this; + } + + @Override + public void remove() { + this.resources.remove(this.cursor); + } + + /** + * 重置游标 + */ + public synchronized void reset() { + this.cursor = 0; + } + + /** + * 增加资源 + * @param resource 资源 + * @return this + */ + public MultiResource add(Resource resource) { + this.resources.add(resource); + return this; + } + +} diff --git a/src/main/java/cn/hutool/core/io/resource/NoResourceException.java b/src/main/java/cn/hutool/core/io/resource/NoResourceException.java new file mode 100644 index 0000000..1959376 --- /dev/null +++ b/src/main/java/cn/hutool/core/io/resource/NoResourceException.java @@ -0,0 +1,47 @@ +package cn.hutool.core.io.resource; + +import cn.hutool.core.exceptions.ExceptionUtil; +import cn.hutool.core.io.IORuntimeException; +import cn.hutool.core.util.StrUtil; + +/** + * 资源文件或资源不存在异常 + * + * @author xiaoleilu + * @since 4.0.2 + */ +public class NoResourceException extends IORuntimeException { + private static final long serialVersionUID = -623254467603299129L; + + public NoResourceException(Throwable e) { + super(ExceptionUtil.getMessage(e), e); + } + + public NoResourceException(String message) { + super(message); + } + + public NoResourceException(String messageTemplate, Object... params) { + super(StrUtil.format(messageTemplate, params)); + } + + public NoResourceException(String message, Throwable throwable) { + super(message, throwable); + } + + public NoResourceException(Throwable throwable, String messageTemplate, Object... params) { + super(StrUtil.format(messageTemplate, params), throwable); + } + + /** + * 导致这个异常的异常是否是指定类型的异常 + * + * @param clazz 异常类 + * @return 是否为指定类型异常 + */ + @Override + public boolean causeInstanceOf(Class clazz) { + final Throwable cause = this.getCause(); + return clazz.isInstance(cause); + } +} diff --git a/src/main/java/cn/hutool/core/io/resource/Resource.java b/src/main/java/cn/hutool/core/io/resource/Resource.java new file mode 100644 index 0000000..b979458 --- /dev/null +++ b/src/main/java/cn/hutool/core/io/resource/Resource.java @@ -0,0 +1,125 @@ +package cn.hutool.core.io.resource; + +import cn.hutool.core.io.IORuntimeException; +import cn.hutool.core.io.IoUtil; +import cn.hutool.core.util.CharsetUtil; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.URL; +import java.nio.charset.Charset; + +/** + * 资源接口定义
+ *

资源是数据表示的统称,我们可以将任意的数据封装为一个资源,然后读取其内容。

+ *

资源可以是文件、URL、ClassPath中的文件亦或者jar(zip)包中的文件。

+ *

+ * 提供资源接口的意义在于,我们可以使用一个方法接收任意类型的数据,从而处理数据, + * 无需专门针对File、InputStream等写多个重载方法,同时也为更好的扩展提供了可能。 + *

+ *

使用非常简单,假设我们需要从classpath中读取一个xml,我们不用关心这个文件在目录中还是在jar中:

+ *
+ *     Resource resource = new ClassPathResource("test.xml");
+ *     String xmlStr = resource.readUtf8Str();
+ * 
+ *

同样,我们可以自己实现Resource接口,按照业务需要从任意位置读取数据,比如从数据库中。

+ * + * @author looly + * @since 3.2.1 + */ +public interface Resource { + + /** + * 获取资源名,例如文件资源的资源名为文件名 + * + * @return 资源名 + * @since 4.0.13 + */ + String getName(); + + /** + * 获得解析后的{@link URL},无对应URL的返回{@code null} + * + * @return 解析后的{@link URL} + */ + URL getUrl(); + + /** + * 获得 {@link InputStream} + * + * @return {@link InputStream} + */ + InputStream getStream(); + + /** + * 检查资源是否变更
+ * 一般用于文件类资源,检查文件是否被修改过。 + * + * @return 是否变更 + * @since 5.7.21 + */ + default boolean isModified(){ + return false; + } + + /** + * 将资源内容写出到流,不关闭输出流,但是关闭资源流 + * + * @param out 输出流 + * @throws IORuntimeException IO异常 + * @since 5.3.5 + */ + default void writeTo(OutputStream out) throws IORuntimeException { + try (InputStream in = getStream()) { + IoUtil.copy(in, out); + } catch (IOException e) { + throw new IORuntimeException(e); + } + } + + /** + * 获得Reader + * + * @param charset 编码 + * @return {@link BufferedReader} + */ + default BufferedReader getReader(Charset charset) { + return IoUtil.getReader(getStream(), charset); + } + + /** + * 读取资源内容,读取完毕后会关闭流
+ * 关闭流并不影响下一次读取 + * + * @param charset 编码 + * @return 读取资源内容 + * @throws IORuntimeException 包装{@link IOException} + */ + default String readStr(Charset charset) throws IORuntimeException { + return IoUtil.read(getReader(charset)); + } + + /** + * 读取资源内容,读取完毕后会关闭流
+ * 关闭流并不影响下一次读取 + * + * @return 读取资源内容 + * @throws IORuntimeException 包装IOException + */ + default String readUtf8Str() throws IORuntimeException { + return readStr(CharsetUtil.CHARSET_UTF_8); + } + + /** + * 读取资源内容,读取完毕后会关闭流
+ * 关闭流并不影响下一次读取 + * + * @return 读取资源内容 + * @throws IORuntimeException 包装IOException + */ + default byte[] readBytes() throws IORuntimeException { + return IoUtil.readBytes(getStream()); + } +} diff --git a/src/main/java/cn/hutool/core/io/resource/ResourceUtil.java b/src/main/java/cn/hutool/core/io/resource/ResourceUtil.java new file mode 100644 index 0000000..8b482e7 --- /dev/null +++ b/src/main/java/cn/hutool/core/io/resource/ResourceUtil.java @@ -0,0 +1,232 @@ +package cn.hutool.core.io.resource; + +import cn.hutool.core.collection.EnumerationIter; +import cn.hutool.core.collection.IterUtil; +import cn.hutool.core.io.FileUtil; +import cn.hutool.core.io.IORuntimeException; +import cn.hutool.core.lang.Filter; +import cn.hutool.core.util.CharsetUtil; +import cn.hutool.core.util.ClassLoaderUtil; +import cn.hutool.core.util.ObjUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.core.util.URLUtil; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.nio.charset.Charset; +import java.util.Enumeration; +import java.util.List; + +/** + * Resource资源工具类 + * + * @author Looly + */ +public class ResourceUtil { + + /** + * 读取Classpath下的资源为字符串,使用UTF-8编码 + * + * @param resource 资源路径,使用相对ClassPath的路径 + * @return 资源内容 + * @since 3.1.1 + */ + public static String readUtf8Str(String resource) { + return getResourceObj(resource).readUtf8Str(); + } + + /** + * 读取Classpath下的资源为字符串 + * + * @param resource 可以是绝对路径,也可以是相对路径(相对ClassPath) + * @param charset 编码 + * @return 资源内容 + * @since 3.1.1 + */ + public static String readStr(String resource, Charset charset) { + return getResourceObj(resource).readStr(charset); + } + + /** + * 读取Classpath下的资源为byte[] + * + * @param resource 可以是绝对路径,也可以是相对路径(相对ClassPath) + * @return 资源内容 + * @since 4.5.19 + */ + public static byte[] readBytes(String resource) { + return getResourceObj(resource).readBytes(); + } + + /** + * 从ClassPath资源中获取{@link InputStream} + * + * @param resource ClassPath资源 + * @return {@link InputStream} + * @throws NoResourceException 资源不存在异常 + * @since 3.1.2 + */ + public static InputStream getStream(String resource) throws NoResourceException { + return getResourceObj(resource).getStream(); + } + + /** + * 从ClassPath资源中获取{@link InputStream},当资源不存在时返回null + * + * @param resource ClassPath资源 + * @return {@link InputStream} + * @since 4.0.3 + */ + public static InputStream getStreamSafe(String resource) { + try { + return getResourceObj(resource).getStream(); + } catch (NoResourceException e) { + // ignore + } + return null; + } + + /** + * 从ClassPath资源中获取{@link BufferedReader} + * + * @param resource ClassPath资源 + * @return {@link InputStream} + * @since 5.3.6 + */ + public static BufferedReader getUtf8Reader(String resource) { + return getReader(resource, CharsetUtil.CHARSET_UTF_8); + } + + /** + * 从ClassPath资源中获取{@link BufferedReader} + * + * @param resource ClassPath资源 + * @param charset 编码 + * @return {@link InputStream} + * @since 3.1.2 + */ + public static BufferedReader getReader(String resource, Charset charset) { + return getResourceObj(resource).getReader(charset); + } + + /** + * 获得资源的URL
+ * 路径用/分隔,例如: + * + *
+	 * config/a/db.config
+	 * spring/xml/test.xml
+	 * 
+ * + * @param resource 资源(相对Classpath的路径) + * @return 资源URL + */ + public static URL getResource(String resource) throws IORuntimeException { + return getResource(resource, null); + } + + /** + * 获取指定路径下的资源列表
+ * 路径格式必须为目录格式,用/分隔,例如: + * + *
+	 * config/a
+	 * spring/xml
+	 * 
+ * + * @param resource 资源路径 + * @return 资源列表 + */ + public static List getResources(String resource) { + return getResources(resource, null); + } + + /** + * 获取指定路径下的资源列表
+ * 路径格式必须为目录格式,用/分隔,例如: + * + *
+	 * config/a
+	 * spring/xml
+	 * 
+ * + * @param resource 资源路径 + * @param filter 过滤器,用于过滤不需要的资源,{@code null}表示不过滤,保留所有元素 + * @return 资源列表 + */ + public static List getResources(String resource, Filter filter) { + return IterUtil.filterToList(getResourceIter(resource), filter); + } + + /** + * 获取指定路径下的资源Iterator
+ * 路径格式必须为目录格式,用/分隔,例如: + * + *
+	 * config/a
+	 * spring/xml
+	 * 
+ * + * @param resource 资源路径 + * @return 资源列表 + * @since 4.1.5 + */ + public static EnumerationIter getResourceIter(String resource) { + return getResourceIter(resource, null); + } + + /** + * 获取指定路径下的资源Iterator
+ * 路径格式必须为目录格式,用/分隔,例如: + * + *
+	 * config/a
+	 * spring/xml
+	 * 
+ * + * @param resource 资源路径 + * @param classLoader {@link ClassLoader} + * @return 资源列表 + * @since 4.1.5 + */ + public static EnumerationIter getResourceIter(String resource, ClassLoader classLoader) { + final Enumeration resources; + try { + resources = ObjUtil.defaultIfNull(classLoader, ClassLoaderUtil::getClassLoader).getResources(resource); + } catch (final IOException e) { + throw new IORuntimeException(e); + } + return new EnumerationIter<>(resources); + } + + /** + * 获得资源相对路径对应的URL + * + * @param resource 资源相对路径,{@code null}和""都表示classpath根路径 + * @param baseClass 基准Class,获得的相对路径相对于此Class所在路径,如果为{@code null}则相对ClassPath + * @return {@link URL} + */ + public static URL getResource(String resource, Class baseClass) { + resource = StrUtil.nullToEmpty(resource); + return (null != baseClass) ? baseClass.getResource(resource) : ClassLoaderUtil.getClassLoader().getResource(resource); + } + + /** + * 获取{@link Resource} 资源对象
+ * 如果提供路径为绝对路径或路径以file:开头,返回{@link FileResource},否则返回{@link ClassPathResource} + * + * @param path 路径,可以是绝对路径,也可以是相对路径(相对ClassPath) + * @return {@link Resource} 资源对象 + * @since 3.2.1 + */ + public static Resource getResourceObj(String path) { + if (StrUtil.isNotBlank(path)) { + if (path.startsWith(URLUtil.FILE_URL_PREFIX) || FileUtil.isAbsolutePath(path)) { + return new FileResource(path); + } + } + return new ClassPathResource(path); + } +} diff --git a/src/main/java/cn/hutool/core/io/resource/StringResource.java b/src/main/java/cn/hutool/core/io/resource/StringResource.java new file mode 100644 index 0000000..540056f --- /dev/null +++ b/src/main/java/cn/hutool/core/io/resource/StringResource.java @@ -0,0 +1,47 @@ +package cn.hutool.core.io.resource; + +import cn.hutool.core.util.CharsetUtil; + +import java.nio.charset.Charset; + +/** + * 字符串资源,字符串做为资源 + * + * @author looly + * @since 4.1.0 + * @see CharSequenceResource + */ +public class StringResource extends CharSequenceResource { + private static final long serialVersionUID = 1L; + + + /** + * 构造,使用UTF8编码 + * + * @param data 资源数据 + */ + public StringResource(String data) { + super(data, null); + } + + /** + * 构造,使用UTF8编码 + * + * @param data 资源数据 + * @param name 资源名称 + */ + public StringResource(String data, String name) { + super(data, name, CharsetUtil.CHARSET_UTF_8); + } + + /** + * 构造 + * + * @param data 资源数据 + * @param name 资源名称 + * @param charset 编码 + */ + public StringResource(String data, String name, Charset charset) { + super(data, name, charset); + } +} diff --git a/src/main/java/cn/hutool/core/io/resource/UrlResource.java b/src/main/java/cn/hutool/core/io/resource/UrlResource.java new file mode 100644 index 0000000..21f24b2 --- /dev/null +++ b/src/main/java/cn/hutool/core/io/resource/UrlResource.java @@ -0,0 +1,107 @@ +package cn.hutool.core.io.resource; + +import cn.hutool.core.io.FileUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.URLUtil; + +import java.io.File; +import java.io.InputStream; +import java.io.Serializable; +import java.net.URI; +import java.net.URL; + +/** + * URL资源访问类 + * @author Looly + * + */ +public class UrlResource implements Resource, Serializable{ + private static final long serialVersionUID = 1L; + + protected URL url; + private long lastModified = 0; + protected String name; + + //-------------------------------------------------------------------------------------- Constructor start + /** + * 构造 + * @param uri URI + * @since 5.7.21 + */ + public UrlResource(URI uri) { + this(URLUtil.url(uri), null); + } + + /** + * 构造 + * @param url URL + */ + public UrlResource(URL url) { + this(url, null); + } + + /** + * 构造 + * @param url URL,允许为空 + * @param name 资源名称 + */ + public UrlResource(URL url, String name) { + this.url = url; + if(null != url && URLUtil.URL_PROTOCOL_FILE.equals(url.getProtocol())){ + this.lastModified = FileUtil.file(url).lastModified(); + } + this.name = ObjectUtil.defaultIfNull(name, () -> (null != url ? FileUtil.getName(url.getPath()) : null)); + } + + /** + * 构造 + * @param file 文件路径 + * @deprecated Please use {@link FileResource} + */ + @Deprecated + public UrlResource(File file) { + this.url = URLUtil.getURL(file); + } + //-------------------------------------------------------------------------------------- Constructor end + + @Override + public String getName() { + return this.name; + } + + @Override + public URL getUrl(){ + return this.url; + } + + @Override + public InputStream getStream() throws NoResourceException{ + if(null == this.url){ + throw new NoResourceException("Resource URL is null!"); + } + return URLUtil.getStream(url); + } + + @Override + public boolean isModified() { + // lastModified == 0表示此资源非文件资源 + return (0 != this.lastModified) && this.lastModified != getFile().lastModified(); + } + + /** + * 获得File + * @return {@link File} + */ + public File getFile(){ + return FileUtil.file(this.url); + } + + /** + * 返回路径 + * @return 返回URL路径 + */ + @Override + public String toString() { + return (null == this.url) ? "null" : this.url.toString(); + } +} diff --git a/src/main/java/cn/hutool/core/io/resource/VfsResource.java b/src/main/java/cn/hutool/core/io/resource/VfsResource.java new file mode 100644 index 0000000..2295d97 --- /dev/null +++ b/src/main/java/cn/hutool/core/io/resource/VfsResource.java @@ -0,0 +1,107 @@ +package cn.hutool.core.io.resource; + +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.ClassLoaderUtil; +import cn.hutool.core.util.ReflectUtil; + +import java.io.InputStream; +import java.lang.reflect.Method; +import java.net.URL; + +/** + * VFS资源封装
+ * 支持VFS 3.x on JBoss AS 6+,JBoss AS 7 and WildFly 8+
+ * 参考:org.springframework.core.io.VfsUtils + * + * @author looly, Spring + * @since 5.7.21 + */ +public class VfsResource implements Resource { + private static final String VFS3_PKG = "org.jboss.vfs."; + + private static final Method VIRTUAL_FILE_METHOD_EXISTS; + private static final Method VIRTUAL_FILE_METHOD_GET_INPUT_STREAM; + private static final Method VIRTUAL_FILE_METHOD_GET_SIZE; + private static final Method VIRTUAL_FILE_METHOD_GET_LAST_MODIFIED; + private static final Method VIRTUAL_FILE_METHOD_TO_URL; + private static final Method VIRTUAL_FILE_METHOD_GET_NAME; + + static { + Class virtualFile = ClassLoaderUtil.loadClass(VFS3_PKG + "VirtualFile"); + try { + VIRTUAL_FILE_METHOD_EXISTS = virtualFile.getMethod("exists"); + VIRTUAL_FILE_METHOD_GET_INPUT_STREAM = virtualFile.getMethod("openStream"); + VIRTUAL_FILE_METHOD_GET_SIZE = virtualFile.getMethod("getSize"); + VIRTUAL_FILE_METHOD_GET_LAST_MODIFIED = virtualFile.getMethod("getLastModified"); + VIRTUAL_FILE_METHOD_TO_URL = virtualFile.getMethod("toURL"); + VIRTUAL_FILE_METHOD_GET_NAME = virtualFile.getMethod("getName"); + } catch (NoSuchMethodException ex) { + throw new IllegalStateException("Could not detect JBoss VFS infrastructure", ex); + } + } + + /** + * org.jboss.vfs.VirtualFile实例对象 + */ + private final Object virtualFile; + private final long lastModified; + + /** + * 构造 + * + * @param resource org.jboss.vfs.VirtualFile实例对象 + */ + public VfsResource(Object resource) { + Assert.notNull(resource, "VirtualFile must not be null"); + this.virtualFile = resource; + this.lastModified = getLastModified(); + } + + /** + * VFS文件是否存在 + * + * @return 文件是否存在 + */ + public boolean exists() { + return ReflectUtil.invoke(virtualFile, VIRTUAL_FILE_METHOD_EXISTS); + } + + @Override + public String getName() { + return ReflectUtil.invoke(virtualFile, VIRTUAL_FILE_METHOD_GET_NAME); + } + + @Override + public URL getUrl() { + return ReflectUtil.invoke(virtualFile, VIRTUAL_FILE_METHOD_TO_URL); + } + + @Override + public InputStream getStream() { + return ReflectUtil.invoke(virtualFile, VIRTUAL_FILE_METHOD_GET_INPUT_STREAM); + } + + @Override + public boolean isModified() { + return this.lastModified != getLastModified(); + } + + /** + * 获得VFS文件最后修改时间 + * + * @return 最后修改时间 + */ + public long getLastModified() { + return ReflectUtil.invoke(virtualFile, VIRTUAL_FILE_METHOD_GET_LAST_MODIFIED); + } + + /** + * 获取VFS文件大小 + * + * @return VFS文件大小 + */ + public long size() { + return ReflectUtil.invoke(virtualFile, VIRTUAL_FILE_METHOD_GET_SIZE); + } + +} diff --git a/src/main/java/cn/hutool/core/io/resource/WebAppResource.java b/src/main/java/cn/hutool/core/io/resource/WebAppResource.java new file mode 100644 index 0000000..eacae8a --- /dev/null +++ b/src/main/java/cn/hutool/core/io/resource/WebAppResource.java @@ -0,0 +1,25 @@ +package cn.hutool.core.io.resource; + +import java.io.File; + +import cn.hutool.core.io.FileUtil; + +/** + * Web root资源访问对象 + * + * @author looly + * @since 4.1.11 + */ +public class WebAppResource extends FileResource { + private static final long serialVersionUID = 1L; + + /** + * 构造 + * + * @param path 相对于Web root的路径 + */ + public WebAppResource(String path) { + super(new File(FileUtil.getWebRoot(), path)); + } + +} diff --git a/src/main/java/cn/hutool/core/io/resource/package-info.java b/src/main/java/cn/hutool/core/io/resource/package-info.java new file mode 100644 index 0000000..cd01639 --- /dev/null +++ b/src/main/java/cn/hutool/core/io/resource/package-info.java @@ -0,0 +1,7 @@ +/** + * 针对ClassPath和文件中资源读取的封装,主要入口为工具类ResourceUtil + * + * @author looly + * + */ +package cn.hutool.core.io.resource; \ No newline at end of file diff --git a/src/main/java/cn/hutool/core/io/unit/DataSize.java b/src/main/java/cn/hutool/core/io/unit/DataSize.java new file mode 100644 index 0000000..00a8e6b --- /dev/null +++ b/src/main/java/cn/hutool/core/io/unit/DataSize.java @@ -0,0 +1,292 @@ +package cn.hutool.core.io.unit; + +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.StrUtil; + +import java.math.BigDecimal; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * 数据大小,可以将类似于'12MB'表示转换为bytes长度的数字 + *

+ * 此类来自于:Spring-framework + * + *

+ *     byte        1B     1
+ *     kilobyte    1KB    1,024
+ *     megabyte    1MB    1,048,576
+ *     gigabyte    1GB    1,073,741,824
+ *     terabyte    1TB    1,099,511,627,776
+ * 
+ * + * @author Sam Brannen,Stephane Nicoll + * @since 5.3.10 + */ +public final class DataSize implements Comparable { + + /** + * The pattern for parsing. + */ + private static final Pattern PATTERN = Pattern.compile("^([+-]?\\d+(\\.\\d+)?)([a-zA-Z]{0,2})$"); + + /** + * Bytes per Kilobyte(KB). + */ + private static final long BYTES_PER_KB = 1024; + + /** + * Bytes per Megabyte(MB). + */ + private static final long BYTES_PER_MB = BYTES_PER_KB * 1024; + + /** + * Bytes per Gigabyte(GB). + */ + private static final long BYTES_PER_GB = BYTES_PER_MB * 1024; + + /** + * Bytes per Terabyte(TB). + */ + private static final long BYTES_PER_TB = BYTES_PER_GB * 1024; + + + /** + * bytes长度 + */ + private final long bytes; + + + /** + * 构造 + * + * @param bytes 长度 + */ + private DataSize(long bytes) { + this.bytes = bytes; + } + + + /** + * 获得对应bytes的DataSize + * + * @param bytes bytes大小,可正可负 + * @return this + */ + public static DataSize ofBytes(long bytes) { + return new DataSize(bytes); + } + + /** + * 获得对应kilobytes的DataSize + * + * @param kilobytes kilobytes大小,可正可负 + * @return a DataSize + */ + public static DataSize ofKilobytes(long kilobytes) { + return new DataSize(Math.multiplyExact(kilobytes, BYTES_PER_KB)); + } + + /** + * 获得对应megabytes的DataSize + * + * @param megabytes megabytes大小,可正可负 + * @return a DataSize + */ + public static DataSize ofMegabytes(long megabytes) { + return new DataSize(Math.multiplyExact(megabytes, BYTES_PER_MB)); + } + + /** + * 获得对应gigabytes的DataSize + * + * @param gigabytes gigabytes大小,可正可负 + * @return a DataSize + */ + public static DataSize ofGigabytes(long gigabytes) { + return new DataSize(Math.multiplyExact(gigabytes, BYTES_PER_GB)); + } + + /** + * 获得对应terabytes的DataSize + * + * @param terabytes terabytes大小,可正可负 + * @return a DataSize + */ + public static DataSize ofTerabytes(long terabytes) { + return new DataSize(Math.multiplyExact(terabytes, BYTES_PER_TB)); + } + + /** + * 获得指定{@link DataUnit}对应的DataSize + * + * @param amount 大小 + * @param unit 数据大小单位,null表示默认的BYTES + * @return DataSize + */ + public static DataSize of(long amount, DataUnit unit) { + if(null == unit){ + unit = DataUnit.BYTES; + } + return new DataSize(Math.multiplyExact(amount, unit.size().toBytes())); + } + + /** + * 获得指定{@link DataUnit}对应的DataSize + * + * @param amount 大小 + * @param unit 数据大小单位,null表示默认的BYTES + * @return DataSize + * @since 5.4.5 + */ + public static DataSize of(BigDecimal amount, DataUnit unit) { + if(null == unit){ + unit = DataUnit.BYTES; + } + return new DataSize(amount.multiply(new BigDecimal(unit.size().toBytes())).longValue()); + } + + /** + * 获取指定数据大小文本对应的DataSize对象,如果无单位指定,默认获取{@link DataUnit#BYTES} + *

+ * 例如: + *

+	 * "12KB" -- parses as "12 kilobytes"
+	 * "5MB"  -- parses as "5 megabytes"
+	 * "20"   -- parses as "20 bytes"
+	 * 
+ * + * @param text the text to parse + * @return the parsed DataSize + * @see #parse(CharSequence, DataUnit) + */ + public static DataSize parse(CharSequence text) { + return parse(text, null); + } + + /** + * Obtain a DataSize from a text string such as {@code 12MB} using + * the specified default {@link DataUnit} if no unit is specified. + *

+ * The string starts with a number followed optionally by a unit matching one of the + * supported {@linkplain DataUnit suffixes}. + *

+ * Examples: + *

+	 * "12KB" -- parses as "12 kilobytes"
+	 * "5MB"  -- parses as "5 megabytes"
+	 * "20"   -- parses as "20 kilobytes" (where the {@code defaultUnit} is {@link DataUnit#KILOBYTES})
+	 * 
+ * + * @param text the text to parse + * @param defaultUnit 默认的数据单位 + * @return the parsed DataSize + */ + public static DataSize parse(CharSequence text, DataUnit defaultUnit) { + Assert.notNull(text, "Text must not be null"); + try { + final Matcher matcher = PATTERN.matcher(text); + Assert.state(matcher.matches(), "Does not match data size pattern"); + + final DataUnit unit = determineDataUnit(matcher.group(3), defaultUnit); + return DataSize.of(new BigDecimal(matcher.group(1)), unit); + } catch (Exception ex) { + throw new IllegalArgumentException("'" + text + "' is not a valid data size", ex); + } + } + + /** + * 决定数据单位,后缀不识别时使用默认单位 + * @param suffix 后缀 + * @param defaultUnit 默认单位 + * @return {@link DataUnit} + */ + private static DataUnit determineDataUnit(String suffix, DataUnit defaultUnit) { + DataUnit defaultUnitToUse = (defaultUnit != null ? defaultUnit : DataUnit.BYTES); + return (StrUtil.isNotEmpty(suffix) ? DataUnit.fromSuffix(suffix) : defaultUnitToUse); + } + + /** + * 是否为负数,不包括0 + * + * @return 负数返回true,否则false + */ + public boolean isNegative() { + return this.bytes < 0; + } + + /** + * 返回bytes大小 + * + * @return bytes大小 + */ + public long toBytes() { + return this.bytes; + } + + /** + * 返回KB大小 + * + * @return KB大小 + */ + public long toKilobytes() { + return this.bytes / BYTES_PER_KB; + } + + /** + * 返回MB大小 + * + * @return MB大小 + */ + public long toMegabytes() { + return this.bytes / BYTES_PER_MB; + } + + /** + * 返回GB大小 + * + * @return GB大小 + * + */ + public long toGigabytes() { + return this.bytes / BYTES_PER_GB; + } + + /** + * 返回TB大小 + * + * @return TB大小 + */ + public long toTerabytes() { + return this.bytes / BYTES_PER_TB; + } + + @Override + public int compareTo(DataSize other) { + return Long.compare(this.bytes, other.bytes); + } + + @Override + public String toString() { + return String.format("%dB", this.bytes); + } + + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (other == null || getClass() != other.getClass()) { + return false; + } + DataSize otherSize = (DataSize) other; + return (this.bytes == otherSize.bytes); + } + + @Override + public int hashCode() { + return Long.hashCode(this.bytes); + } + +} diff --git a/src/main/java/cn/hutool/core/io/unit/DataSizeUtil.java b/src/main/java/cn/hutool/core/io/unit/DataSizeUtil.java new file mode 100644 index 0000000..2835a80 --- /dev/null +++ b/src/main/java/cn/hutool/core/io/unit/DataSizeUtil.java @@ -0,0 +1,38 @@ +package cn.hutool.core.io.unit; + +import java.text.DecimalFormat; + +/** + * 数据大小工具类 + * + * @author looly + * @since 5.3.10 + */ +public class DataSizeUtil { + + /** + * 解析数据大小字符串,转换为bytes大小 + * + * @param text 数据大小字符串,类似于:12KB, 5MB等 + * @return bytes大小 + */ + public static long parse(String text) { + return DataSize.parse(text).toBytes(); + } + + /** + * 可读的文件大小
+ * 参考 http://stackoverflow.com/questions/3263892/format-file-size-as-mb-gb-etc + * + * @param size Long类型大小 + * @return 大小 + */ + public static String format(long size) { + if (size <= 0) { + return "0"; + } + int digitGroups = Math.min(DataUnit.UNIT_NAMES.length-1, (int) (Math.log10(size) / Math.log10(1024))); + return new DecimalFormat("#,##0.##") + .format(size / Math.pow(1024, digitGroups)) + " " + DataUnit.UNIT_NAMES[digitGroups]; + } +} diff --git a/src/main/java/cn/hutool/core/io/unit/DataUnit.java b/src/main/java/cn/hutool/core/io/unit/DataUnit.java new file mode 100644 index 0000000..2163fc9 --- /dev/null +++ b/src/main/java/cn/hutool/core/io/unit/DataUnit.java @@ -0,0 +1,80 @@ +package cn.hutool.core.io.unit; + +import cn.hutool.core.util.StrUtil; + +/** + * 数据单位封装

+ * 此类来自于:Spring-framework + * + *

+ *     BYTES      1B      2^0     1
+ *     KILOBYTES  1KB     2^10    1,024
+ *     MEGABYTES  1MB     2^20    1,048,576
+ *     GIGABYTES  1GB     2^30    1,073,741,824
+ *     TERABYTES  1TB     2^40    1,099,511,627,776
+ * 
+ * + * @author Sam Brannen,Stephane Nicoll + * @since 5.3.10 + */ +public enum DataUnit { + + /** + * Bytes, 后缀表示为: {@code B}. + */ + BYTES("B", DataSize.ofBytes(1)), + + /** + * Kilobytes, 后缀表示为: {@code KB}. + */ + KILOBYTES("KB", DataSize.ofKilobytes(1)), + + /** + * Megabytes, 后缀表示为: {@code MB}. + */ + MEGABYTES("MB", DataSize.ofMegabytes(1)), + + /** + * Gigabytes, 后缀表示为: {@code GB}. + */ + GIGABYTES("GB", DataSize.ofGigabytes(1)), + + /** + * Terabytes, 后缀表示为: {@code TB}. + */ + TERABYTES("TB", DataSize.ofTerabytes(1)); + + public static final String[] UNIT_NAMES = new String[]{"B", "KB", "MB", "GB", "TB", "PB", "EB"}; + + private final String suffix; + + private final DataSize size; + + + DataUnit(String suffix, DataSize size) { + this.suffix = suffix; + this.size = size; + } + + DataSize size() { + return this.size; + } + + /** + * 通过后缀返回对应的 DataUnit + * + * @param suffix 单位后缀 + * @return 匹配到的{@link DataUnit} + * @throws IllegalArgumentException 后缀无法识别报错 + */ + public static DataUnit fromSuffix(String suffix) { + for (DataUnit candidate : values()) { + // 支持类似于 3MB,3M,3m等写法 + if (StrUtil.startWithIgnoreCase(candidate.suffix, suffix)) { + return candidate; + } + } + throw new IllegalArgumentException("Unknown data unit suffix '" + suffix + "'"); + } + +} diff --git a/src/main/java/cn/hutool/core/io/unit/package-info.java b/src/main/java/cn/hutool/core/io/unit/package-info.java new file mode 100644 index 0000000..c5f6693 --- /dev/null +++ b/src/main/java/cn/hutool/core/io/unit/package-info.java @@ -0,0 +1,7 @@ +/** + * 数据单位相关封装,包括DataUnit数据单位和DataSize数据大小 + * + * @author looly + * @since 5.3.10 + */ +package cn.hutool.core.io.unit; \ No newline at end of file diff --git a/src/main/java/cn/hutool/core/io/watch/SimpleWatcher.java b/src/main/java/cn/hutool/core/io/watch/SimpleWatcher.java new file mode 100644 index 0000000..3a1b614 --- /dev/null +++ b/src/main/java/cn/hutool/core/io/watch/SimpleWatcher.java @@ -0,0 +1,13 @@ +package cn.hutool.core.io.watch; + +import cn.hutool.core.io.watch.watchers.IgnoreWatcher; + +/** + * 空白WatchListener
+ * 用户继承此类后实现需要监听的方法 + * @author Looly + * + */ +public class SimpleWatcher extends IgnoreWatcher{ + +} diff --git a/src/main/java/cn/hutool/core/io/watch/WatchAction.java b/src/main/java/cn/hutool/core/io/watch/WatchAction.java new file mode 100644 index 0000000..5b6d17a --- /dev/null +++ b/src/main/java/cn/hutool/core/io/watch/WatchAction.java @@ -0,0 +1,24 @@ +package cn.hutool.core.io.watch; + +import java.nio.file.Path; +import java.nio.file.WatchEvent; + +/** + * 监听事件处理函数接口 + * + * @author looly + * @since 5.4.0 + */ +@FunctionalInterface +public interface WatchAction { + + /** + * 事件处理,通过实现此方法处理各种事件。 + * + * 事件可以调用 {@link WatchEvent#kind()}获取,对应事件见{@link WatchKind} + * + * @param event 事件 + * @param currentPath 事件发生的当前Path路径 + */ + void doAction(WatchEvent event, Path currentPath); +} diff --git a/src/main/java/cn/hutool/core/io/watch/WatchException.java b/src/main/java/cn/hutool/core/io/watch/WatchException.java new file mode 100644 index 0000000..b2df4e2 --- /dev/null +++ b/src/main/java/cn/hutool/core/io/watch/WatchException.java @@ -0,0 +1,33 @@ +package cn.hutool.core.io.watch; + +import cn.hutool.core.exceptions.ExceptionUtil; +import cn.hutool.core.util.StrUtil; + +/** + * 监听异常 + * @author Looly + * + */ +public class WatchException extends RuntimeException { + private static final long serialVersionUID = 8068509879445395353L; + + public WatchException(Throwable e) { + super(ExceptionUtil.getMessage(e), e); + } + + public WatchException(String message) { + super(message); + } + + public WatchException(String messageTemplate, Object... params) { + super(StrUtil.format(messageTemplate, params)); + } + + public WatchException(String message, Throwable throwable) { + super(message, throwable); + } + + public WatchException(Throwable throwable, String messageTemplate, Object... params) { + super(StrUtil.format(messageTemplate, params), throwable); + } +} diff --git a/src/main/java/cn/hutool/core/io/watch/WatchKind.java b/src/main/java/cn/hutool/core/io/watch/WatchKind.java new file mode 100644 index 0000000..fe2fa55 --- /dev/null +++ b/src/main/java/cn/hutool/core/io/watch/WatchKind.java @@ -0,0 +1,67 @@ +package cn.hutool.core.io.watch; + +import java.nio.file.StandardWatchEventKinds; +import java.nio.file.WatchEvent; + +/** + * 监听事件类型枚举,包括: + * + *
+ *      1. 事件丢失 OVERFLOW -》StandardWatchEventKinds.OVERFLOW
+ *      2. 修改事件 MODIFY   -》StandardWatchEventKinds.ENTRY_MODIFY
+ *      3. 创建事件 CREATE   -》StandardWatchEventKinds.ENTRY_CREATE
+ *      4. 删除事件 DELETE   -》StandardWatchEventKinds.ENTRY_DELETE
+ * 
+ * + * @author loolly + * @since 5.1.0 + */ +public enum WatchKind { + + /** + * 事件丢失 + */ + OVERFLOW(StandardWatchEventKinds.OVERFLOW), + /** + * 修改事件 + */ + MODIFY(StandardWatchEventKinds.ENTRY_MODIFY), + /** + * 创建事件 + */ + CREATE(StandardWatchEventKinds.ENTRY_CREATE), + /** + * 删除事件 + */ + DELETE(StandardWatchEventKinds.ENTRY_DELETE); + + /** + * 全部事件 + */ + public static final WatchEvent.Kind[] ALL = {// + OVERFLOW.getValue(), //事件丢失 + MODIFY.getValue(), //修改 + CREATE.getValue(), //创建 + DELETE.getValue() //删除 + }; + + private final WatchEvent.Kind value; + + /** + * 构造 + * + * @param value 事件类型 + */ + WatchKind(WatchEvent.Kind value) { + this.value = value; + } + + /** + * 获取枚举对应的事件类型 + * + * @return 事件类型值 + */ + public WatchEvent.Kind getValue() { + return this.value; + } +} diff --git a/src/main/java/cn/hutool/core/io/watch/WatchMonitor.java b/src/main/java/cn/hutool/core/io/watch/WatchMonitor.java new file mode 100644 index 0000000..d24bab1 --- /dev/null +++ b/src/main/java/cn/hutool/core/io/watch/WatchMonitor.java @@ -0,0 +1,426 @@ +package cn.hutool.core.io.watch; + +import cn.hutool.core.io.FileUtil; +import cn.hutool.core.io.IORuntimeException; +import cn.hutool.core.io.watch.watchers.WatcherChain; +import cn.hutool.core.util.StrUtil; +import cn.hutool.core.util.URLUtil; + +import java.io.File; +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.LinkOption; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.WatchEvent; +import java.nio.file.WatchService; + +/** + * 路径监听器 + * + *

+ * 监听器可监听目录或文件
+ * 如果监听的Path不存在,则递归创建空目录然后监听此空目录
+ * 递归监听目录时,并不会监听新创建的目录 + * + * @author Looly + */ +public class WatchMonitor extends WatchServer { + private static final long serialVersionUID = 1L; + + /** + * 事件丢失 + */ + public static final WatchEvent.Kind OVERFLOW = WatchKind.OVERFLOW.getValue(); + /** + * 修改事件 + */ + public static final WatchEvent.Kind ENTRY_MODIFY = WatchKind.MODIFY.getValue(); + /** + * 创建事件 + */ + public static final WatchEvent.Kind ENTRY_CREATE = WatchKind.CREATE.getValue(); + /** + * 删除事件 + */ + public static final WatchEvent.Kind ENTRY_DELETE = WatchKind.DELETE.getValue(); + /** + * 全部事件 + */ + public static final WatchEvent.Kind[] EVENTS_ALL = WatchKind.ALL; + + /** + * 监听路径,必须为目录 + */ + private Path path; + /** + * 递归目录的最大深度,当小于1时不递归下层目录 + */ + private int maxDepth; + /** + * 监听的文件,对于单文件监听不为空 + */ + private Path filePath; + + /** + * 监听器 + */ + private Watcher watcher; + //------------------------------------------------------ Static method start + + /** + * 创建并初始化监听 + * + * @param url URL + * @param events 监听的事件列表 + * @return 监听对象 + */ + public static WatchMonitor create(URL url, WatchEvent.Kind... events) { + return create(url, 0, events); + } + + /** + * 创建并初始化监听 + * + * @param url URL + * @param events 监听的事件列表 + * @param maxDepth 当监听目录时,监听目录的最大深度,当设置值为1(或小于1)时,表示不递归监听子目录 + * @return 监听对象 + */ + public static WatchMonitor create(URL url, int maxDepth, WatchEvent.Kind... events) { + return create(URLUtil.toURI(url), maxDepth, events); + } + + /** + * 创建并初始化监听 + * + * @param uri URI + * @param events 监听的事件列表 + * @return 监听对象 + */ + public static WatchMonitor create(URI uri, WatchEvent.Kind... events) { + return create(uri, 0, events); + } + + /** + * 创建并初始化监听 + * + * @param uri URI + * @param events 监听的事件列表 + * @param maxDepth 当监听目录时,监听目录的最大深度,当设置值为1(或小于1)时,表示不递归监听子目录 + * @return 监听对象 + */ + public static WatchMonitor create(URI uri, int maxDepth, WatchEvent.Kind... events) { + return create(Paths.get(uri), maxDepth, events); + } + + /** + * 创建并初始化监听 + * + * @param file 文件 + * @param events 监听的事件列表 + * @return 监听对象 + */ + public static WatchMonitor create(File file, WatchEvent.Kind... events) { + return create(file, 0, events); + } + + /** + * 创建并初始化监听 + * + * @param file 文件 + * @param events 监听的事件列表 + * @param maxDepth 当监听目录时,监听目录的最大深度,当设置值为1(或小于1)时,表示不递归监听子目录 + * @return 监听对象 + */ + public static WatchMonitor create(File file, int maxDepth, WatchEvent.Kind... events) { + return create(file.toPath(), maxDepth, events); + } + + /** + * 创建并初始化监听 + * + * @param path 路径 + * @param events 监听的事件列表 + * @return 监听对象 + */ + public static WatchMonitor create(String path, WatchEvent.Kind... events) { + return create(path, 0, events); + } + + /** + * 创建并初始化监听 + * + * @param path 路径 + * @param events 监听的事件列表 + * @param maxDepth 当监听目录时,监听目录的最大深度,当设置值为1(或小于1)时,表示不递归监听子目录 + * @return 监听对象 + */ + public static WatchMonitor create(String path, int maxDepth, WatchEvent.Kind... events) { + return create(Paths.get(path), maxDepth, events); + } + + /** + * 创建并初始化监听 + * + * @param path 路径 + * @param events 监听事件列表 + * @return 监听对象 + */ + public static WatchMonitor create(Path path, WatchEvent.Kind... events) { + return create(path, 0, events); + } + + /** + * 创建并初始化监听 + * + * @param path 路径 + * @param events 监听事件列表 + * @param maxDepth 当监听目录时,监听目录的最大深度,当设置值为1(或小于1)时,表示不递归监听子目录 + * @return 监听对象 + */ + public static WatchMonitor create(Path path, int maxDepth, WatchEvent.Kind... events) { + return new WatchMonitor(path, maxDepth, events); + } + + //--------- createAll + + /** + * 创建并初始化监听,监听所有事件 + * + * @param uri URI + * @param watcher {@link Watcher} + * @return WatchMonitor + */ + public static WatchMonitor createAll(URI uri, Watcher watcher) { + return createAll(Paths.get(uri), watcher); + } + + /** + * 创建并初始化监听,监听所有事件 + * + * @param url URL + * @param watcher {@link Watcher} + * @return WatchMonitor + */ + public static WatchMonitor createAll(URL url, Watcher watcher) { + try { + return createAll(Paths.get(url.toURI()), watcher); + } catch (URISyntaxException e) { + throw new WatchException(e); + } + } + + /** + * 创建并初始化监听,监听所有事件 + * + * @param file 被监听文件 + * @param watcher {@link Watcher} + * @return WatchMonitor + */ + public static WatchMonitor createAll(File file, Watcher watcher) { + return createAll(file.toPath(), watcher); + } + + /** + * 创建并初始化监听,监听所有事件 + * + * @param path 路径 + * @param watcher {@link Watcher} + * @return WatchMonitor + */ + public static WatchMonitor createAll(String path, Watcher watcher) { + return createAll(Paths.get(path), watcher); + } + + /** + * 创建并初始化监听,监听所有事件 + * + * @param path 路径 + * @param watcher {@link Watcher} + * @return WatchMonitor + */ + public static WatchMonitor createAll(Path path, Watcher watcher) { + final WatchMonitor watchMonitor = create(path, EVENTS_ALL); + watchMonitor.setWatcher(watcher); + return watchMonitor; + } + //------------------------------------------------------ Static method end + + //------------------------------------------------------ Constructor method start + + /** + * 构造 + * + * @param file 文件 + * @param events 监听的事件列表 + */ + public WatchMonitor(File file, WatchEvent.Kind... events) { + this(file.toPath(), events); + } + + /** + * 构造 + * + * @param path 字符串路径 + * @param events 监听的事件列表 + */ + public WatchMonitor(String path, WatchEvent.Kind... events) { + this(Paths.get(path), events); + } + + /** + * 构造 + * + * @param path 字符串路径 + * @param events 监听事件列表 + */ + public WatchMonitor(Path path, WatchEvent.Kind... events) { + this(path, 0, events); + } + + /** + * 构造
+ * 例如设置: + *

+	 * maxDepth <= 1 表示只监听当前目录
+	 * maxDepth = 2 表示监听当前目录以及下层目录
+	 * maxDepth = 3 表示监听当前目录以及下两层
+	 * 
+ * + * @param path 字符串路径 + * @param maxDepth 递归目录的最大深度,当小于2时不递归下层目录 + * @param events 监听事件列表 + */ + public WatchMonitor(Path path, int maxDepth, WatchEvent.Kind... events) { + this.path = path; + this.maxDepth = maxDepth; + this.events = events; + this.init(); + } + //------------------------------------------------------ Constructor method end + + /** + * 初始化
+ * 初始化包括: + *
+	 * 1、解析传入的路径,判断其为目录还是文件
+	 * 2、创建{@link WatchService} 对象
+	 * 
+ * + * @throws WatchException 监听异常,IO异常时抛出此异常 + */ + @Override + public void init() throws WatchException { + //获取目录或文件路径 + if (!Files.exists(this.path, LinkOption.NOFOLLOW_LINKS)) { + // 不存在的路径 + final Path lastPathEle = FileUtil.getLastPathEle(this.path); + if (null != lastPathEle) { + final String lastPathEleStr = lastPathEle.toString(); + //带有点表示有扩展名,按照未创建的文件对待。Linux下.d的为目录,排除之 + if (StrUtil.contains(lastPathEleStr, StrUtil.C_DOT) && !StrUtil.endWithIgnoreCase(lastPathEleStr, ".d")) { + this.filePath = this.path; + this.path = this.filePath.getParent(); + } + } + + //创建不存在的目录或父目录 + try { + Files.createDirectories(this.path); + } catch (IOException e) { + throw new IORuntimeException(e); + } + } else if (Files.isRegularFile(this.path, LinkOption.NOFOLLOW_LINKS)) { + // 文件路径 + this.filePath = this.path; + this.path = this.filePath.getParent(); + } + + super.init(); + } + + /** + * 设置监听
+ * 多个监听请使用{@link WatcherChain} + * + * @param watcher 监听 + * @return WatchMonitor + */ + public WatchMonitor setWatcher(Watcher watcher) { + this.watcher = watcher; + return this; + } + + @Override + public void run() { + watch(); + } + + /** + * 开始监听事件,阻塞当前进程 + */ + public void watch() { + watch(this.watcher); + } + + /** + * 开始监听事件,阻塞当前进程 + * + * @param watcher 监听 + * @throws WatchException 监听异常,如果监听关闭抛出此异常 + */ + public void watch(Watcher watcher) throws WatchException { + if (isClosed) { + throw new WatchException("Watch Monitor is closed !"); + } + + // 按照层级注册路径及其子路径 + registerPath(); +// log.debug("Start watching path: [{}]", this.path); + + while (!isClosed) { + doTakeAndWatch(watcher); + } + } + + /** + * 当监听目录时,监听目录的最大深度
+ * 当设置值为1(或小于1)时,表示不递归监听子目录
+ * 例如设置: + *
+	 * maxDepth <= 1 表示只监听当前目录
+	 * maxDepth = 2 表示监听当前目录以及下层目录
+	 * maxDepth = 3 表示监听当前目录以及下层
+	 * 
+ * + * @param maxDepth 最大深度,当设置值为1(或小于1)时,表示不递归监听子目录,监听所有子目录请传{@link Integer#MAX_VALUE} + * @return this + */ + public WatchMonitor setMaxDepth(int maxDepth) { + this.maxDepth = maxDepth; + return this; + } + + //------------------------------------------------------ private method start + + /** + * 执行事件获取并处理 + * + * @param watcher {@link Watcher} + */ + private void doTakeAndWatch(Watcher watcher) { + super.watch(watcher, watchEvent -> null == filePath || filePath.endsWith(watchEvent.context().toString())); + } + + /** + * 注册监听路径 + */ + private void registerPath() { + registerPath(this.path, (null != this.filePath) ? 0 : this.maxDepth); + } + //------------------------------------------------------ private method end +} diff --git a/src/main/java/cn/hutool/core/io/watch/WatchServer.java b/src/main/java/cn/hutool/core/io/watch/WatchServer.java new file mode 100644 index 0000000..3eb2712 --- /dev/null +++ b/src/main/java/cn/hutool/core/io/watch/WatchServer.java @@ -0,0 +1,189 @@ +package cn.hutool.core.io.watch; + +import cn.hutool.core.io.IoUtil; +import cn.hutool.core.lang.Filter; +import cn.hutool.core.util.ArrayUtil; + +import java.io.Closeable; +import java.io.IOException; +import java.io.Serializable; +import java.nio.file.AccessDeniedException; +import java.nio.file.ClosedWatchServiceException; +import java.nio.file.FileSystems; +import java.nio.file.FileVisitOption; +import java.nio.file.FileVisitResult; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.WatchEvent; +import java.nio.file.WatchKey; +import java.nio.file.WatchService; +import java.util.EnumSet; +import java.util.HashMap; +import java.util.Map; + +/** + * 文件监听服务,此服务可以同时监听多个路径。 + * + * @author loolly + * @since 5.1.0 + */ +public class WatchServer extends Thread implements Closeable, Serializable { + private static final long serialVersionUID = 1L; + + /** + * 监听服务 + */ + private WatchService watchService; + /** + * 监听事件列表 + */ + protected WatchEvent.Kind[] events; + /** + * 监听选项,例如监听频率等 + */ + private WatchEvent.Modifier[] modifiers; + /** + * 监听是否已经关闭 + */ + protected boolean isClosed; + /** + * WatchKey 和 Path的对应表 + */ + private final Map watchKeyPathMap = new HashMap<>(); + + /** + * 初始化
+ * 初始化包括: + *
+	 * 1、解析传入的路径,判断其为目录还是文件
+	 * 2、创建{@link WatchService} 对象
+	 * 
+ * + * @throws WatchException 监听异常,IO异常时抛出此异常 + */ + public void init() throws WatchException { + //初始化监听 + try { + watchService = FileSystems.getDefault().newWatchService(); + } catch (IOException e) { + throw new WatchException(e); + } + + isClosed = false; + } + + /** + * 设置监听选项,例如监听频率等,可设置项包括: + * + *
+	 * 1、com.sun.nio.file.StandardWatchEventKinds
+	 * 2、com.sun.nio.file.SensitivityWatchEventModifier
+	 * 
+ * + * @param modifiers 监听选项,例如监听频率等 + */ + public void setModifiers(WatchEvent.Modifier[] modifiers) { + this.modifiers = modifiers; + } + + /** + * 将指定路径加入到监听中 + * + * @param path 路径 + * @param maxDepth 递归下层目录的最大深度 + */ + public void registerPath(Path path, int maxDepth) { + final WatchEvent.Kind[] kinds = ArrayUtil.defaultIfEmpty(this.events, WatchKind.ALL); + + try { + final WatchKey key; + if (ArrayUtil.isEmpty(this.modifiers)) { + key = path.register(this.watchService, kinds); + } else { + key = path.register(this.watchService, kinds, this.modifiers); + } + watchKeyPathMap.put(key, path); + + // 递归注册下一层层级的目录 + if (maxDepth > 1) { + //遍历所有子目录并加入监听 + Files.walkFileTree(path, EnumSet.noneOf(FileVisitOption.class), maxDepth, new SimpleFileVisitor() { + @Override + public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException { + registerPath(dir, 0);//继续添加目录 + return super.postVisitDirectory(dir, exc); + } + }); + } + } catch (IOException e) { + if (!(e instanceof AccessDeniedException)) { + throw new WatchException(e); + } + + //对于禁止访问的目录,跳过监听 + } + } + + /** + * 执行事件获取并处理 + * + * @param action 监听回调函数,实现此函数接口用于处理WatchEvent事件 + * @param watchFilter 监听过滤接口,通过实现此接口过滤掉不需要监听的情况,null表示不过滤 + * @since 5.4.0 + */ + public void watch(WatchAction action, Filter> watchFilter) { + WatchKey wk; + try { + wk = watchService.take(); + } catch (InterruptedException | ClosedWatchServiceException e) { + // 用户中断 + close(); + return; + } + + final Path currentPath = watchKeyPathMap.get(wk); + + for (WatchEvent event : wk.pollEvents()) { + // 如果监听文件,检查当前事件是否与所监听文件关联 + if (null != watchFilter && !watchFilter.accept(event)) { + continue; + } + + action.doAction(event, currentPath); + } + + wk.reset(); + } + + /** + * 执行事件获取并处理 + * + * @param watcher {@link Watcher} + * @param watchFilter 监听过滤接口,通过实现此接口过滤掉不需要监听的情况,null表示不过滤 + */ + public void watch(Watcher watcher, Filter> watchFilter) { + watch((event, currentPath)->{ + final WatchEvent.Kind kind = event.kind(); + + if (kind == WatchKind.CREATE.getValue()) { + watcher.onCreate(event, currentPath); + } else if (kind == WatchKind.MODIFY.getValue()) { + watcher.onModify(event, currentPath); + } else if (kind == WatchKind.DELETE.getValue()) { + watcher.onDelete(event, currentPath); + } else if (kind == WatchKind.OVERFLOW.getValue()) { + watcher.onOverflow(event, currentPath); + } + }, watchFilter); + } + + /** + * 关闭监听 + */ + @Override + public void close() { + isClosed = true; + IoUtil.close(watchService); + } +} diff --git a/src/main/java/cn/hutool/core/io/watch/WatchUtil.java b/src/main/java/cn/hutool/core/io/watch/WatchUtil.java new file mode 100644 index 0000000..bddcf8b --- /dev/null +++ b/src/main/java/cn/hutool/core/io/watch/WatchUtil.java @@ -0,0 +1,397 @@ +package cn.hutool.core.io.watch; + +import java.io.File; +import java.io.IOException; +import java.net.URI; +import java.net.URL; +import java.nio.file.*; + +import cn.hutool.core.io.IORuntimeException; +import cn.hutool.core.util.URLUtil; + +/** + * 监听工具类
+ * 主要负责文件监听器的快捷创建 + * + * @author Looly + * @since 3.1.0 + */ +public class WatchUtil { + /** + * 创建并初始化监听 + * + * @param url URL + * @param events 监听的事件列表 + * @return 监听对象 + */ + public static WatchMonitor create(URL url, WatchEvent.Kind... events) { + return create(url, 0, events); + } + + /** + * 创建并初始化监听 + * + * @param url URL + * @param events 监听的事件列表 + * @param maxDepth 当监听目录时,监听目录的最大深度,当设置值为1(或小于1)时,表示不递归监听子目录 + * @return 监听对象 + */ + public static WatchMonitor create(URL url, int maxDepth, WatchEvent.Kind... events) { + return create(URLUtil.toURI(url), maxDepth, events); + } + + /** + * 创建并初始化监听 + * + * @param uri URI + * @param events 监听的事件列表 + * @return 监听对象 + */ + public static WatchMonitor create(URI uri, WatchEvent.Kind... events) { + return create(uri, 0, events); + } + + /** + * 创建并初始化监听 + * + * @param uri URI + * @param events 监听的事件列表 + * @param maxDepth 当监听目录时,监听目录的最大深度,当设置值为1(或小于1)时,表示不递归监听子目录 + * @return 监听对象 + */ + public static WatchMonitor create(URI uri, int maxDepth, WatchEvent.Kind... events) { + return create(Paths.get(uri), maxDepth, events); + } + + /** + * 创建并初始化监听 + * + * @param file 文件 + * @param events 监听的事件列表 + * @return 监听对象 + */ + public static WatchMonitor create(File file, WatchEvent.Kind... events) { + return create(file, 0, events); + } + + /** + * 创建并初始化监听 + * + * @param file 文件 + * @param events 监听的事件列表 + * @param maxDepth 当监听目录时,监听目录的最大深度,当设置值为1(或小于1)时,表示不递归监听子目录 + * @return 监听对象 + */ + public static WatchMonitor create(File file, int maxDepth, WatchEvent.Kind... events) { + return create(file.toPath(), maxDepth, events); + } + + /** + * 创建并初始化监听 + * + * @param path 路径 + * @param events 监听的事件列表 + * @return 监听对象 + */ + public static WatchMonitor create(String path, WatchEvent.Kind... events) { + return create(path, 0, events); + } + + /** + * 创建并初始化监听 + * + * @param path 路径 + * @param events 监听的事件列表 + * @param maxDepth 当监听目录时,监听目录的最大深度,当设置值为1(或小于1)时,表示不递归监听子目录 + * @return 监听对象 + */ + public static WatchMonitor create(String path, int maxDepth, WatchEvent.Kind... events) { + return create(Paths.get(path), maxDepth, events); + } + + /** + * 创建并初始化监听 + * + * @param path 路径 + * @param events 监听事件列表 + * @return 监听对象 + */ + public static WatchMonitor create(Path path, WatchEvent.Kind... events) { + return create(path, 0, events); + } + + /** + * 创建并初始化监听 + * + * @param path 路径 + * @param events 监听事件列表 + * @param maxDepth 当监听目录时,监听目录的最大深度,当设置值为1(或小于1)时,表示不递归监听子目录 + * @return 监听对象 + */ + public static WatchMonitor create(Path path, int maxDepth, WatchEvent.Kind... events) { + return new WatchMonitor(path, maxDepth, events); + } + + // ---------------------------------------------------------------------------------------------------------- createAll + /** + * 创建并初始化监听,监听所有事件 + * + * @param url URL + * @param watcher {@link Watcher} + * @return {@link WatchMonitor} + */ + public static WatchMonitor createAll(URL url, Watcher watcher) { + return createAll(url, 0, watcher); + } + + /** + * 创建并初始化监听,监听所有事件 + * + * @param url URL + * @param maxDepth 当监听目录时,监听目录的最大深度,当设置值为1(或小于1)时,表示不递归监听子目录 + * @param watcher {@link Watcher} + * @return {@link WatchMonitor} + */ + public static WatchMonitor createAll(URL url, int maxDepth, Watcher watcher) { + return createAll(URLUtil.toURI(url), maxDepth, watcher); + } + + /** + * 创建并初始化监听,监听所有事件 + * + * @param uri URI + * @param watcher {@link Watcher} + * @return {@link WatchMonitor} + */ + public static WatchMonitor createAll(URI uri, Watcher watcher) { + return createAll(uri, 0, watcher); + } + + /** + * 创建并初始化监听,监听所有事件 + * + * @param uri URI + * @param maxDepth 当监听目录时,监听目录的最大深度,当设置值为1(或小于1)时,表示不递归监听子目录 + * @param watcher {@link Watcher} + * @return {@link WatchMonitor} + */ + public static WatchMonitor createAll(URI uri, int maxDepth, Watcher watcher) { + return createAll(Paths.get(uri), maxDepth, watcher); + } + + /** + * 创建并初始化监听,监听所有事件 + * + * @param file 被监听文件 + * @param watcher {@link Watcher} + * @return {@link WatchMonitor} + */ + public static WatchMonitor createAll(File file, Watcher watcher) { + return createAll(file, 0, watcher); + } + + /** + * 创建并初始化监听,监听所有事件 + * + * @param file 被监听文件 + * @param maxDepth 当监听目录时,监听目录的最大深度,当设置值为1(或小于1)时,表示不递归监听子目录 + * @param watcher {@link Watcher} + * @return {@link WatchMonitor} + */ + public static WatchMonitor createAll(File file, int maxDepth, Watcher watcher) { + return createAll(file.toPath(), 0, watcher); + } + + /** + * 创建并初始化监听,监听所有事件 + * + * @param path 路径 + * @param watcher {@link Watcher} + * @return {@link WatchMonitor} + */ + public static WatchMonitor createAll(String path, Watcher watcher) { + return createAll(path, 0, watcher); + } + + /** + * 创建并初始化监听,监听所有事件 + * + * @param path 路径 + * @param maxDepth 当监听目录时,监听目录的最大深度,当设置值为1(或小于1)时,表示不递归监听子目录 + * @param watcher {@link Watcher} + * @return {@link WatchMonitor} + */ + public static WatchMonitor createAll(String path, int maxDepth, Watcher watcher) { + return createAll(Paths.get(path), maxDepth, watcher); + } + + /** + * 创建并初始化监听,监听所有事件 + * + * @param path 路径 + * @param watcher {@link Watcher} + * @return {@link WatchMonitor} + */ + public static WatchMonitor createAll(Path path, Watcher watcher) { + return createAll(path, 0, watcher); + } + + /** + * 创建并初始化监听,监听所有事件 + * + * @param path 路径 + * @param maxDepth 当监听目录时,监听目录的最大深度,当设置值为1(或小于1)时,表示不递归监听子目录 + * @param watcher {@link Watcher} + * @return {@link WatchMonitor} + */ + public static WatchMonitor createAll(Path path, int maxDepth, Watcher watcher) { + final WatchMonitor watchMonitor = create(path, maxDepth, WatchMonitor.EVENTS_ALL); + watchMonitor.setWatcher(watcher); + return watchMonitor; + } + + // ---------------------------------------------------------------------------------------------------------- createModify + /** + * 创建并初始化监听,监听修改事件 + * + * @param url URL + * @param watcher {@link Watcher} + * @return {@link WatchMonitor} + * @since 4.5.2 + */ + public static WatchMonitor createModify(URL url, Watcher watcher) { + return createModify(url, 0, watcher); + } + + /** + * 创建并初始化监听,监听修改事件 + * + * @param url URL + * @param maxDepth 当监听目录时,监听目录的最大深度,当设置值为1(或小于1)时,表示不递归监听子目录 + * @param watcher {@link Watcher} + * @return {@link WatchMonitor} + * @since 4.5.2 + */ + public static WatchMonitor createModify(URL url, int maxDepth, Watcher watcher) { + return createModify(URLUtil.toURI(url), maxDepth, watcher); + } + + /** + * 创建并初始化监听,监听修改事件 + * + * @param uri URI + * @param watcher {@link Watcher} + * @return {@link WatchMonitor} + * @since 4.5.2 + */ + public static WatchMonitor createModify(URI uri, Watcher watcher) { + return createModify(uri, 0, watcher); + } + + /** + * 创建并初始化监听,监听修改事件 + * + * @param uri URI + * @param maxDepth 当监听目录时,监听目录的最大深度,当设置值为1(或小于1)时,表示不递归监听子目录 + * @param watcher {@link Watcher} + * @return {@link WatchMonitor} + * @since 4.5.2 + */ + public static WatchMonitor createModify(URI uri, int maxDepth, Watcher watcher) { + return createModify(Paths.get(uri), maxDepth, watcher); + } + + /** + * 创建并初始化监听,监听修改事件 + * + * @param file 被监听文件 + * @param watcher {@link Watcher} + * @return {@link WatchMonitor} + * @since 4.5.2 + */ + public static WatchMonitor createModify(File file, Watcher watcher) { + return createModify(file, 0, watcher); + } + + /** + * 创建并初始化监听,监听修改事件 + * + * @param file 被监听文件 + * @param maxDepth 当监听目录时,监听目录的最大深度,当设置值为1(或小于1)时,表示不递归监听子目录 + * @param watcher {@link Watcher} + * @return {@link WatchMonitor} + * @since 4.5.2 + */ + public static WatchMonitor createModify(File file, int maxDepth, Watcher watcher) { + return createModify(file.toPath(), 0, watcher); + } + + /** + * 创建并初始化监听,监听修改事件 + * + * @param path 路径 + * @param watcher {@link Watcher} + * @return {@link WatchMonitor} + * @since 4.5.2 + */ + public static WatchMonitor createModify(String path, Watcher watcher) { + return createModify(path, 0, watcher); + } + + /** + * 创建并初始化监听,监听修改事件 + * + * @param path 路径 + * @param maxDepth 当监听目录时,监听目录的最大深度,当设置值为1(或小于1)时,表示不递归监听子目录 + * @param watcher {@link Watcher} + * @return {@link WatchMonitor} + * @since 4.5.2 + */ + public static WatchMonitor createModify(String path, int maxDepth, Watcher watcher) { + return createModify(Paths.get(path), maxDepth, watcher); + } + + /** + * 创建并初始化监听,监听修改事件 + * + * @param path 路径 + * @param watcher {@link Watcher} + * @return {@link WatchMonitor} + * @since 4.5.2 + */ + public static WatchMonitor createModify(Path path, Watcher watcher) { + return createModify(path, 0, watcher); + } + + /** + * 创建并初始化监听,监听修改事件 + * + * @param path 路径 + * @param maxDepth 当监听目录时,监听目录的最大深度,当设置值为1(或小于1)时,表示不递归监听子目录 + * @param watcher {@link Watcher} + * @return {@link WatchMonitor} + * @since 4.5.2 + */ + public static WatchMonitor createModify(Path path, int maxDepth, Watcher watcher) { + final WatchMonitor watchMonitor = create(path, maxDepth, WatchMonitor.ENTRY_MODIFY); + watchMonitor.setWatcher(watcher); + return watchMonitor; + } + + /** + * 注册Watchable对象到WatchService服务 + * + * @param watchable 可注册对象 + * @param watcher WatchService对象 + * @param events 监听事件 + * @return {@link WatchKey} + * @since 4.6.9 + */ + public static WatchKey register(Watchable watchable, WatchService watcher, WatchEvent.Kind... events){ + try { + return watchable.register(watcher, events); + } catch (IOException e) { + throw new IORuntimeException(e); + } + } +} diff --git a/src/main/java/cn/hutool/core/io/watch/Watcher.java b/src/main/java/cn/hutool/core/io/watch/Watcher.java new file mode 100644 index 0000000..dc8237b --- /dev/null +++ b/src/main/java/cn/hutool/core/io/watch/Watcher.java @@ -0,0 +1,44 @@ +package cn.hutool.core.io.watch; + +import java.nio.file.Path; +import java.nio.file.WatchEvent; + +/** + * 观察者(监视器) + * + * @author Looly + */ +public interface Watcher { + /** + * 文件创建时执行的方法 + * + * @param event 事件 + * @param currentPath 事件发生的当前Path路径 + */ + void onCreate(WatchEvent event, Path currentPath); + + /** + * 文件修改时执行的方法
+ * 文件修改可能触发多次 + * + * @param event 事件 + * @param currentPath 事件发生的当前Path路径 + */ + void onModify(WatchEvent event, Path currentPath); + + /** + * 文件删除时执行的方法 + * + * @param event 事件 + * @param currentPath 事件发生的当前Path路径 + */ + void onDelete(WatchEvent event, Path currentPath); + + /** + * 事件丢失或出错时执行的方法 + * + * @param event 事件 + * @param currentPath 事件发生的当前Path路径 + */ + void onOverflow(WatchEvent event, Path currentPath); +} diff --git a/src/main/java/cn/hutool/core/io/watch/package-info.java b/src/main/java/cn/hutool/core/io/watch/package-info.java new file mode 100644 index 0000000..f476322 --- /dev/null +++ b/src/main/java/cn/hutool/core/io/watch/package-info.java @@ -0,0 +1,7 @@ +/** + * 基于JDK7+ WatchService的文件和目录监听封装,支持多级目录 + * + * @author looly + * + */ +package cn.hutool.core.io.watch; \ No newline at end of file diff --git a/src/main/java/cn/hutool/core/io/watch/watchers/DelayWatcher.java b/src/main/java/cn/hutool/core/io/watch/watchers/DelayWatcher.java new file mode 100644 index 0000000..4d6ee25 --- /dev/null +++ b/src/main/java/cn/hutool/core/io/watch/watchers/DelayWatcher.java @@ -0,0 +1,105 @@ +package cn.hutool.core.io.watch.watchers; + +import cn.hutool.core.collection.ConcurrentHashSet; +import cn.hutool.core.io.watch.Watcher; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.thread.ThreadUtil; + +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.WatchEvent; +import java.nio.file.WatchService; +import java.util.Set; + +/** + * 延迟观察者
+ * 使用此观察者通过定义一定的延迟时间,解决{@link WatchService}多个modify的问题
+ * 在监听目录或文件时,如果这个文件有修改操作,会多次触发modify方法。
+ * 此类通过维护一个Set将短时间内相同文件多次modify的事件合并处理触发,从而避免以上问题。
+ * 注意:延迟只针对modify事件,其它事件无效 + * + * @author Looly + * @since 3.1.0 + */ +public class DelayWatcher implements Watcher { + + /** Path集合。此集合用于去重在指定delay内多次触发的文件Path */ + private final Set eventSet = new ConcurrentHashSet<>(); + /** 实际处理 */ + private final Watcher watcher; + /** 延迟,单位毫秒 */ + private final long delay; + + //---------------------------------------------------------------------------------------------------------- Constructor start + /** + * 构造 + * @param watcher 实际处理触发事件的监视器{@link Watcher},不可以是{@link DelayWatcher} + * @param delay 延迟时间,单位毫秒 + */ + public DelayWatcher(Watcher watcher, long delay) { + Assert.notNull(watcher); + if(watcher instanceof DelayWatcher) { + throw new IllegalArgumentException("Watcher must not be a DelayWatcher"); + } + this.watcher = watcher; + this.delay = delay; + } + //---------------------------------------------------------------------------------------------------------- Constructor end + + @Override + public void onModify(WatchEvent event, Path currentPath) { + if(this.delay < 1) { + this.watcher.onModify(event, currentPath); + }else { + onDelayModify(event, currentPath); + } + } + + @Override + public void onCreate(WatchEvent event, Path currentPath) { + watcher.onCreate(event, currentPath); + } + + @Override + public void onDelete(WatchEvent event, Path currentPath) { + watcher.onDelete(event, currentPath); + } + + @Override + public void onOverflow(WatchEvent event, Path currentPath) { + watcher.onOverflow(event, currentPath); + } + + //---------------------------------------------------------------------------------------------------------- Private method start + /** + * 触发延迟修改 + * @param event 事件 + * @param currentPath 事件发生的当前Path路径 + */ + private void onDelayModify(WatchEvent event, Path currentPath) { + Path eventPath = Paths.get(currentPath.toString(), event.context().toString()); + if(eventSet.contains(eventPath)) { + //此事件已经被触发过,后续事件忽略,等待统一处理。 + return; + } + + //事件第一次触发,此时标记事件,并启动处理线程延迟处理,处理结束后会删除标记 + eventSet.add(eventPath); + startHandleModifyThread(event, currentPath); + } + + /** + * 开启处理线程 + * + * @param event 事件 + * @param currentPath 事件发生的当前Path路径 + */ + private void startHandleModifyThread(final WatchEvent event, final Path currentPath) { + ThreadUtil.execute(() -> { + ThreadUtil.sleep(delay); + eventSet.remove(Paths.get(currentPath.toString(), event.context().toString())); + watcher.onModify(event, currentPath); + }); + } + //---------------------------------------------------------------------------------------------------------- Private method end +} diff --git a/src/main/java/cn/hutool/core/io/watch/watchers/IgnoreWatcher.java b/src/main/java/cn/hutool/core/io/watch/watchers/IgnoreWatcher.java new file mode 100644 index 0000000..3beed73 --- /dev/null +++ b/src/main/java/cn/hutool/core/io/watch/watchers/IgnoreWatcher.java @@ -0,0 +1,32 @@ +package cn.hutool.core.io.watch.watchers; + +import java.nio.file.Path; +import java.nio.file.WatchEvent; + +import cn.hutool.core.io.watch.Watcher; + +/** + * 跳过所有事件处理Watcher
+ * 用户继承此类后实现需要监听的方法 + * + * @author Looly + * @since 3.1.0 + */ +public class IgnoreWatcher implements Watcher { + + @Override + public void onCreate(WatchEvent event, Path currentPath) { + } + + @Override + public void onModify(WatchEvent event, Path currentPath) { + } + + @Override + public void onDelete(WatchEvent event, Path currentPath) { + } + + @Override + public void onOverflow(WatchEvent event, Path currentPath) { + } +} diff --git a/src/main/java/cn/hutool/core/io/watch/watchers/WatcherChain.java b/src/main/java/cn/hutool/core/io/watch/watchers/WatcherChain.java new file mode 100644 index 0000000..18ba2be --- /dev/null +++ b/src/main/java/cn/hutool/core/io/watch/watchers/WatcherChain.java @@ -0,0 +1,81 @@ +package cn.hutool.core.io.watch.watchers; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.io.watch.Watcher; +import cn.hutool.core.lang.Chain; + +import java.nio.file.Path; +import java.nio.file.WatchEvent; +import java.util.Iterator; +import java.util.List; + +/** + * 观察者链
+ * 用于加入多个观察者 + * + * @author Looly + * @since 3.1.0 + */ +public class WatcherChain implements Watcher, Chain{ + + /** 观察者列表 */ + final private List chain; + + /** + * 创建观察者链{@link WatcherChain} + * @param watchers 观察者列表 + * @return {@link WatcherChain} + */ + public static WatcherChain create(Watcher... watchers) { + return new WatcherChain(watchers); + } + + /** + * 构造 + * @param watchers 观察者列表 + */ + public WatcherChain(Watcher... watchers) { + chain = CollUtil.newArrayList(watchers); + } + + @Override + public void onCreate(WatchEvent event, Path currentPath) { + for (Watcher watcher : chain) { + watcher.onCreate(event, currentPath); + } + } + + @Override + public void onModify(WatchEvent event, Path currentPath) { + for (Watcher watcher : chain) { + watcher.onModify(event, currentPath); + } + } + + @Override + public void onDelete(WatchEvent event, Path currentPath) { + for (Watcher watcher : chain) { + watcher.onDelete(event, currentPath); + } + } + + @Override + public void onOverflow(WatchEvent event, Path currentPath) { + for (Watcher watcher : chain) { + watcher.onOverflow(event, currentPath); + } + } + + @SuppressWarnings("NullableProblems") + @Override + public Iterator iterator() { + return this.chain.iterator(); + } + + @Override + public WatcherChain addChain(Watcher element) { + this.chain.add(element); + return this; + } + +} diff --git a/src/main/java/cn/hutool/core/io/watch/watchers/package-info.java b/src/main/java/cn/hutool/core/io/watch/watchers/package-info.java new file mode 100644 index 0000000..db4a524 --- /dev/null +++ b/src/main/java/cn/hutool/core/io/watch/watchers/package-info.java @@ -0,0 +1,7 @@ +/** + * 文件监听中的观察者实现类,包括延迟处理、处理链等 + * + * @author looly + * + */ +package cn.hutool.core.io.watch.watchers; \ No newline at end of file diff --git a/src/main/java/cn/hutool/core/lang/Assert.java b/src/main/java/cn/hutool/core/lang/Assert.java new file mode 100644 index 0000000..b0d061a --- /dev/null +++ b/src/main/java/cn/hutool/core/lang/Assert.java @@ -0,0 +1,1121 @@ +package cn.hutool.core.lang; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; + +import java.util.Map; +import java.util.function.Supplier; + +/** + * 断言
+ * 断言某些对象或值是否符合规定,否则抛出异常。经常用于做变量检查 + * + * @author Looly + */ +public class Assert { + + private static final String TEMPLATE_VALUE_MUST_BE_BETWEEN_AND = "The value must be between {} and {}."; + + + /** + * 断言是否为真,如果为 {@code false} 抛出给定的异常
+ * + *
+	 * Assert.isTrue(i > 0, IllegalArgumentException::new);
+	 * 
+ * + * @param 异常类型 + * @param expression 布尔值 + * @param supplier 指定断言不通过时抛出的异常 + * @throws X if expression is {@code false} + */ + public static void isTrue(boolean expression, Supplier supplier) throws X { + if (!expression) { + throw supplier.get(); + } + } + + /** + * 断言是否为真,如果为 {@code false} 抛出 {@code IllegalArgumentException} 异常
+ * + *
+	 * Assert.isTrue(i > 0, "The value must be greater than zero");
+	 * 
+ * + * @param expression 布尔值 + * @param errorMsgTemplate 错误抛出异常附带的消息模板,变量用{}代替 + * @param params 参数列表 + * @throws IllegalArgumentException if expression is {@code false} + */ + public static void isTrue(boolean expression, String errorMsgTemplate, Object... params) throws IllegalArgumentException { + isTrue(expression, () -> new IllegalArgumentException(StrUtil.format(errorMsgTemplate, params))); + } + + /** + * 断言是否为真,如果为 {@code false} 抛出 {@code IllegalArgumentException} 异常
+ * + *
+	 * Assert.isTrue(i > 0);
+	 * 
+ * + * @param expression 布尔值 + * @throws IllegalArgumentException if expression is {@code false} + */ + public static void isTrue(boolean expression) throws IllegalArgumentException { + isTrue(expression, "[Assertion failed] - this expression must be true"); + } + + /** + * 断言是否为假,如果为 {@code true} 抛出指定类型异常
+ * 并使用指定的函数获取错误信息返回 + *
+	 *  Assert.isFalse(i > 0, ()->{
+	 *      // to query relation message
+	 *      return new IllegalArgumentException("relation message to return");
+	 *  });
+	 * 
+ * + * @param 异常类型 + * @param expression 布尔值 + * @param errorSupplier 指定断言不通过时抛出的异常 + * @throws X if expression is {@code false} + * @since 5.4.5 + */ + public static void isFalse(boolean expression, Supplier errorSupplier) throws X { + if (expression) { + throw errorSupplier.get(); + } + } + + /** + * 断言是否为假,如果为 {@code true} 抛出 {@code IllegalArgumentException} 异常
+ * + *
+	 * Assert.isFalse(i < 0, "The value must not be negative");
+	 * 
+ * + * @param expression 布尔值 + * @param errorMsgTemplate 错误抛出异常附带的消息模板,变量用{}代替 + * @param params 参数列表 + * @throws IllegalArgumentException if expression is {@code false} + */ + public static void isFalse(boolean expression, String errorMsgTemplate, Object... params) throws IllegalArgumentException { + isFalse(expression, () -> new IllegalArgumentException(StrUtil.format(errorMsgTemplate, params))); + } + + /** + * 断言是否为假,如果为 {@code true} 抛出 {@code IllegalArgumentException} 异常
+ * + *
+	 * Assert.isFalse(i < 0);
+	 * 
+ * + * @param expression 布尔值 + * @throws IllegalArgumentException if expression is {@code false} + */ + public static void isFalse(boolean expression) throws IllegalArgumentException { + isFalse(expression, "[Assertion failed] - this expression must be false"); + } + + /** + * 断言对象是否为{@code null} ,如果不为{@code null} 抛出指定类型异常 + * 并使用指定的函数获取错误信息返回 + *
+	 * Assert.isNull(value, ()->{
+	 *      // to query relation message
+	 *      return new IllegalArgumentException("relation message to return");
+	 *  });
+	 * 
+ * + * @param 异常类型 + * @param object 被检查的对象 + * @param errorSupplier 错误抛出异常附带的消息生产接口 + * @throws X if the object is not {@code null} + * @since 5.4.5 + */ + public static void isNull(Object object, Supplier errorSupplier) throws X { + if (null != object) { + throw errorSupplier.get(); + } + } + + /** + * 断言对象是否为{@code null} ,如果不为{@code null} 抛出{@link IllegalArgumentException} 异常 + * + *
+	 * Assert.isNull(value, "The value must be null");
+	 * 
+ * + * @param object 被检查的对象 + * @param errorMsgTemplate 消息模板,变量使用{}表示 + * @param params 参数列表 + * @throws IllegalArgumentException if the object is not {@code null} + */ + public static void isNull(Object object, String errorMsgTemplate, Object... params) throws IllegalArgumentException { + isNull(object, () -> new IllegalArgumentException(StrUtil.format(errorMsgTemplate, params))); + } + + /** + * 断言对象是否为{@code null} ,如果不为{@code null} 抛出{@link IllegalArgumentException} 异常 + * + *
+	 * Assert.isNull(value);
+	 * 
+ * + * @param object 被检查对象 + * @throws IllegalArgumentException if the object is not {@code null} + */ + public static void isNull(Object object) throws IllegalArgumentException { + isNull(object, "[Assertion failed] - the object argument must be null"); + } + + // ----------------------------------------------------------------------------------------------------------- Check not null + + /** + * 断言对象是否不为{@code null} ,如果为{@code null} 抛出指定类型异常 + * 并使用指定的函数获取错误信息返回 + *
+	 * Assert.notNull(clazz, ()->{
+	 *      // to query relation message
+	 *      return new IllegalArgumentException("relation message to return");
+	 *  });
+	 * 
+ * + * @param 被检查对象泛型类型 + * @param 异常类型 + * @param object 被检查对象 + * @param errorSupplier 错误抛出异常附带的消息生产接口 + * @return 被检查后的对象 + * @throws X if the object is {@code null} + * @since 5.4.5 + */ + public static T notNull(T object, Supplier errorSupplier) throws X { + if (null == object) { + throw errorSupplier.get(); + } + return object; + } + + /** + * 断言对象是否不为{@code null} ,如果为{@code null} 抛出{@link IllegalArgumentException} 异常 Assert that an object is not {@code null} . + * + *
+	 * Assert.notNull(clazz, "The class must not be null");
+	 * 
+ * + * @param 被检查对象泛型类型 + * @param object 被检查对象 + * @param errorMsgTemplate 错误消息模板,变量使用{}表示 + * @param params 参数 + * @return 被检查后的对象 + * @throws IllegalArgumentException if the object is {@code null} + */ + public static T notNull(T object, String errorMsgTemplate, Object... params) throws IllegalArgumentException { + return notNull(object, () -> new IllegalArgumentException(StrUtil.format(errorMsgTemplate, params))); + } + + /** + * 断言对象是否不为{@code null} ,如果为{@code null} 抛出{@link IllegalArgumentException} 异常 + * + *
+	 * Assert.notNull(clazz);
+	 * 
+ * + * @param 被检查对象类型 + * @param object 被检查对象 + * @return 非空对象 + * @throws IllegalArgumentException if the object is {@code null} + */ + public static T notNull(T object) throws IllegalArgumentException { + return notNull(object, "[Assertion failed] - this argument is required; it must not be null"); + } + + // ----------------------------------------------------------------------------------------------------------- Check empty + + /** + * 检查给定字符串是否为空,为空抛出自定义异常,并使用指定的函数获取错误信息返回。 + *
+	 * Assert.notEmpty(name, ()->{
+	 *      // to query relation message
+	 *      return new IllegalArgumentException("relation message to return");
+	 *  });
+	 * 
+ * + * @param 异常类型 + * @param 字符串类型 + * @param text 被检查字符串 + * @param errorSupplier 错误抛出异常附带的消息生产接口 + * @return 非空字符串 + * @throws X 被检查字符串为空抛出此异常 + * @see StrUtil#isNotEmpty(CharSequence) + * @since 5.4.5 + */ + public static T notEmpty(T text, Supplier errorSupplier) throws X { + if (StrUtil.isEmpty(text)) { + throw errorSupplier.get(); + } + return text; + } + + /** + * 检查给定字符串是否为空,为空抛出 {@link IllegalArgumentException} + * + *
+	 * Assert.notEmpty(name, "Name must not be empty");
+	 * 
+ * + * @param 字符串类型 + * @param text 被检查字符串 + * @param errorMsgTemplate 错误消息模板,变量使用{}表示 + * @param params 参数 + * @return 非空字符串 + * @throws IllegalArgumentException 被检查字符串为空 + * @see StrUtil#isNotEmpty(CharSequence) + */ + public static T notEmpty(T text, String errorMsgTemplate, Object... params) throws IllegalArgumentException { + return notEmpty(text, () -> new IllegalArgumentException(StrUtil.format(errorMsgTemplate, params))); + } + + /** + * 检查给定字符串是否为空,为空抛出 {@link IllegalArgumentException} + * + *
+	 * Assert.notEmpty(name);
+	 * 
+ * + * @param 字符串类型 + * @param text 被检查字符串 + * @return 被检查的字符串 + * @throws IllegalArgumentException 被检查字符串为空 + * @see StrUtil#isNotEmpty(CharSequence) + */ + public static T notEmpty(T text) throws IllegalArgumentException { + return notEmpty(text, "[Assertion failed] - this String argument must have length; it must not be null or empty"); + } + + /** + * 检查给定字符串是否为空白(null、空串或只包含空白符),为空抛出自定义异常。 + * 并使用指定的函数获取错误信息返回 + *
+	 * Assert.notBlank(name, ()->{
+	 *      // to query relation message
+	 *      return new IllegalArgumentException("relation message to return");
+	 *  });
+	 * 
+ * + * @param 异常类型 + * @param 字符串类型 + * @param text 被检查字符串 + * @param errorMsgSupplier 错误抛出异常附带的消息生产接口 + * @return 非空字符串 + * @throws X 被检查字符串为空白 + * @see StrUtil#isNotBlank(CharSequence) + */ + public static T notBlank(T text, Supplier errorMsgSupplier) throws X { + if (StrUtil.isBlank(text)) { + throw errorMsgSupplier.get(); + } + return text; + } + + /** + * 检查给定字符串是否为空白(null、空串或只包含空白符),为空抛出 {@link IllegalArgumentException} + * + *
+	 * Assert.notBlank(name, "Name must not be blank");
+	 * 
+ * + * @param 字符串类型 + * @param text 被检查字符串 + * @param errorMsgTemplate 错误消息模板,变量使用{}表示 + * @param params 参数 + * @return 非空字符串 + * @throws IllegalArgumentException 被检查字符串为空白 + * @see StrUtil#isNotBlank(CharSequence) + */ + public static T notBlank(T text, String errorMsgTemplate, Object... params) throws IllegalArgumentException { + return notBlank(text, () -> new IllegalArgumentException(StrUtil.format(errorMsgTemplate, params))); + } + + /** + * 检查给定字符串是否为空白(null、空串或只包含空白符),为空抛出 {@link IllegalArgumentException} + * + *
+	 * Assert.notBlank(name);
+	 * 
+ * + * @param 字符串类型 + * @param text 被检查字符串 + * @return 非空字符串 + * @throws IllegalArgumentException 被检查字符串为空白 + * @see StrUtil#isNotBlank(CharSequence) + */ + public static T notBlank(T text) throws IllegalArgumentException { + return notBlank(text, "[Assertion failed] - this String argument must have text; it must not be null, empty, or blank"); + } + + /** + * 断言给定字符串是否不被另一个字符串包含(即是否为子串) + * 并使用指定的函数获取错误信息返回 + *
+	 * Assert.notContain(name, "rod", ()->{
+	 *      // to query relation message
+	 *      return new IllegalArgumentException("relation message to return ");
+	 *  });
+	 * 
+ * + * @param 字符串类型 + * @param 异常类型 + * @param textToSearch 被搜索的字符串 + * @param substring 被检查的子串 + * @param errorSupplier 错误抛出异常附带的消息生产接口 + * @return 被检查的子串 + * @throws X 非子串抛出异常 + * @see StrUtil#contains(CharSequence, CharSequence) + * @since 5.4.5 + */ + public static T notContain(CharSequence textToSearch, T substring, Supplier errorSupplier) throws X { + if (StrUtil.contains(textToSearch, substring)) { + throw errorSupplier.get(); + } + return substring; + } + + /** + * 断言给定字符串是否不被另一个字符串包含(即是否为子串) + * + *
+	 * Assert.notContain(name, "rod", "Name must not contain 'rod'");
+	 * 
+ * + * @param textToSearch 被搜索的字符串 + * @param substring 被检查的子串 + * @param errorMsgTemplate 异常时的消息模板 + * @param params 参数列表 + * @return 被检查的子串 + * @throws IllegalArgumentException 非子串抛出异常 + */ + public static String notContain(String textToSearch, String substring, String errorMsgTemplate, Object... params) throws IllegalArgumentException { + return notContain(textToSearch, substring, () -> new IllegalArgumentException(StrUtil.format(errorMsgTemplate, params))); + } + + /** + * 断言给定字符串是否不被另一个字符串包含(即是否为子串) + * + *
+	 * Assert.notContain(name, "rod");
+	 * 
+ * + * @param textToSearch 被搜索的字符串 + * @param substring 被检查的子串 + * @return 被检查的子串 + * @throws IllegalArgumentException 非子串抛出异常 + */ + public static String notContain(String textToSearch, String substring) throws IllegalArgumentException { + return notContain(textToSearch, substring, "[Assertion failed] - this String argument must not contain the substring [{}]", substring); + } + + /** + * 断言给定数组是否包含元素,数组必须不为 {@code null} 且至少包含一个元素 + * 并使用指定的函数获取错误信息返回 + * + *
+	 * Assert.notEmpty(array, ()->{
+	 *      // to query relation message
+	 *      return new IllegalArgumentException("relation message to return");
+	 *  });
+	 * 
+ * + * @param 数组元素类型 + * @param 异常类型 + * @param array 被检查的数组 + * @param errorSupplier 错误抛出异常附带的消息生产接口 + * @return 被检查的数组 + * @throws X if the object array is {@code null} or has no elements + * @see ArrayUtil#isNotEmpty(Object[]) + * @since 5.4.5 + */ + public static T[] notEmpty(T[] array, Supplier errorSupplier) throws X { + if (ArrayUtil.isEmpty(array)) { + throw errorSupplier.get(); + } + return array; + } + + /** + * 断言给定数组是否包含元素,数组必须不为 {@code null} 且至少包含一个元素 + * + *
+	 * Assert.notEmpty(array, "The array must have elements");
+	 * 
+ * + * @param 数组元素类型 + * @param array 被检查的数组 + * @param errorMsgTemplate 异常时的消息模板 + * @param params 参数列表 + * @return 被检查的数组 + * @throws IllegalArgumentException if the object array is {@code null} or has no elements + */ + public static T[] notEmpty(T[] array, String errorMsgTemplate, Object... params) throws IllegalArgumentException { + return notEmpty(array, () -> new IllegalArgumentException(StrUtil.format(errorMsgTemplate, params))); + } + + /** + * 断言给定数组是否包含元素,数组必须不为 {@code null} 且至少包含一个元素 + * + *
+	 * Assert.notEmpty(array, "The array must have elements");
+	 * 
+ * + * @param 数组元素类型 + * @param array 被检查的数组 + * @return 被检查的数组 + * @throws IllegalArgumentException if the object array is {@code null} or has no elements + */ + public static T[] notEmpty(T[] array) throws IllegalArgumentException { + return notEmpty(array, "[Assertion failed] - this array must not be empty: it must contain at least 1 element"); + } + + /** + * 断言给定数组是否不包含{@code null}元素,如果数组为空或 {@code null}将被认为不包含 + * 并使用指定的函数获取错误信息返回 + *
+	 * Assert.noNullElements(array, ()->{
+	 *      // to query relation message
+	 *      return new IllegalArgumentException("relation message to return ");
+	 *  });
+	 * 
+ * + * @param 数组元素类型 + * @param 异常类型 + * @param array 被检查的数组 + * @param errorSupplier 错误抛出异常附带的消息生产接口 + * @return 被检查的数组 + * @throws X if the object array contains a {@code null} element + * @see ArrayUtil#hasNull(Object[]) + * @since 5.4.5 + */ + public static T[] noNullElements(T[] array, Supplier errorSupplier) throws X { + if (ArrayUtil.hasNull(array)) { + throw errorSupplier.get(); + } + return array; + } + + /** + * 断言给定数组是否不包含{@code null}元素,如果数组为空或 {@code null}将被认为不包含 + * + *
+	 * Assert.noNullElements(array, "The array must not have null elements");
+	 * 
+ * + * @param 数组元素类型 + * @param array 被检查的数组 + * @param errorMsgTemplate 异常时的消息模板 + * @param params 参数列表 + * @return 被检查的数组 + * @throws IllegalArgumentException if the object array contains a {@code null} element + */ + public static T[] noNullElements(T[] array, String errorMsgTemplate, Object... params) throws IllegalArgumentException { + return noNullElements(array, () -> new IllegalArgumentException(StrUtil.format(errorMsgTemplate, params))); + } + + /** + * 断言给定数组是否不包含{@code null}元素,如果数组为空或 {@code null}将被认为不包含 + * + *
+	 * Assert.noNullElements(array);
+	 * 
+ * + * @param 数组元素类型 + * @param array 被检查的数组 + * @return 被检查的数组 + * @throws IllegalArgumentException if the object array contains a {@code null} element + */ + public static T[] noNullElements(T[] array) throws IllegalArgumentException { + return noNullElements(array, "[Assertion failed] - this array must not contain any null elements"); + } + + /** + * 断言给定集合非空 + * 并使用指定的函数获取错误信息返回 + *
+	 * Assert.notEmpty(collection, ()->{
+	 *      // to query relation message
+	 *      return new IllegalArgumentException("relation message to return");
+	 *  });
+	 * 
+ * + * @param 集合元素类型 + * @param 集合类型 + * @param 异常类型 + * @param collection 被检查的集合 + * @param errorSupplier 错误抛出异常附带的消息生产接口 + * @return 非空集合 + * @throws X if the collection is {@code null} or has no elements + * @see CollUtil#isNotEmpty(Iterable) + * @since 5.4.5 + */ + public static , X extends Throwable> T notEmpty(T collection, Supplier errorSupplier) throws X { + if (CollUtil.isEmpty(collection)) { + throw errorSupplier.get(); + } + return collection; + } + + /** + * 断言给定集合非空 + * + *
+	 * Assert.notEmpty(collection, "Collection must have elements");
+	 * 
+ * + * @param 集合元素类型 + * @param 集合类型 + * @param collection 被检查的集合 + * @param errorMsgTemplate 异常时的消息模板 + * @param params 参数列表 + * @return 非空集合 + * @throws IllegalArgumentException if the collection is {@code null} or has no elements + */ + public static > T notEmpty(T collection, String errorMsgTemplate, Object... params) throws IllegalArgumentException { + return notEmpty(collection, () -> new IllegalArgumentException(StrUtil.format(errorMsgTemplate, params))); + } + + /** + * 断言给定集合非空 + * + *
+	 * Assert.notEmpty(collection);
+	 * 
+ * + * @param 集合元素类型 + * @param 集合类型 + * @param collection 被检查的集合 + * @return 被检查集合 + * @throws IllegalArgumentException if the collection is {@code null} or has no elements + */ + public static > T notEmpty(T collection) throws IllegalArgumentException { + return notEmpty(collection, "[Assertion failed] - this collection must not be empty: it must contain at least 1 element"); + } + + /** + * 断言给定Map非空 + * 并使用指定的函数获取错误信息返回 + *
+	 * Assert.notEmpty(map, ()->{
+	 *      // to query relation message
+	 *      return new IllegalArgumentException("relation message to return");
+	 *  });
+	 * 
+ * + * @param Key类型 + * @param Value类型 + * @param Map类型 + * @param 异常类型 + * @param map 被检查的Map + * @param errorSupplier 错误抛出异常附带的消息生产接口 + * @return 被检查的Map + * @throws X if the map is {@code null} or has no entries + * @see MapUtil#isNotEmpty(Map) + * @since 5.4.5 + */ + public static , X extends Throwable> T notEmpty(T map, Supplier errorSupplier) throws X { + if (MapUtil.isEmpty(map)) { + throw errorSupplier.get(); + } + return map; + } + + /** + * 断言给定Map非空 + * + *
+	 * Assert.notEmpty(map, "Map must have entries");
+	 * 
+ * + * @param Key类型 + * @param Value类型 + * @param Map类型 + * @param map 被检查的Map + * @param errorMsgTemplate 异常时的消息模板 + * @param params 参数列表 + * @return 被检查的Map + * @throws IllegalArgumentException if the map is {@code null} or has no entries + */ + public static > T notEmpty(T map, String errorMsgTemplate, Object... params) throws IllegalArgumentException { + return notEmpty(map, () -> new IllegalArgumentException(StrUtil.format(errorMsgTemplate, params))); + } + + /** + * 断言给定Map非空 + * + *
+	 * Assert.notEmpty(map, "Map must have entries");
+	 * 
+ * + * @param Key类型 + * @param Value类型 + * @param Map类型 + * @param map 被检查的Map + * @return 被检查的Map + * @throws IllegalArgumentException if the map is {@code null} or has no entries + */ + public static > T notEmpty(T map) throws IllegalArgumentException { + return notEmpty(map, "[Assertion failed] - this map must not be empty; it must contain at least one entry"); + } + + /** + * 断言给定对象是否是给定类的实例 + * + *
+	 * Assert.instanceOf(Foo.class, foo);
+	 * 
+ * + * @param 被检查对象泛型类型 + * @param type 被检查对象匹配的类型 + * @param obj 被检查对象 + * @return 被检查的对象 + * @throws IllegalArgumentException if the object is not an instance of clazz + * @see Class#isInstance(Object) + */ + public static T isInstanceOf(Class type, T obj) { + return isInstanceOf(type, obj, "Object [{}] is not instanceof [{}]", obj, type); + } + + /** + * 断言给定对象是否是给定类的实例 + * + *
+	 * Assert.instanceOf(Foo.class, foo, "foo must be an instance of class Foo");
+	 * 
+ * + * @param 被检查对象泛型类型 + * @param type 被检查对象匹配的类型 + * @param obj 被检查对象 + * @param errorMsgTemplate 异常时的消息模板 + * @param params 参数列表 + * @return 被检查对象 + * @throws IllegalArgumentException if the object is not an instance of clazz + * @see Class#isInstance(Object) + */ + public static T isInstanceOf(Class type, T obj, String errorMsgTemplate, Object... params) throws IllegalArgumentException { + notNull(type, "Type to check against must not be null"); + if (!type.isInstance(obj)) { + throw new IllegalArgumentException(StrUtil.format(errorMsgTemplate, params)); + } + return obj; + } + + /** + * 断言 {@code superType.isAssignableFrom(subType)} 是否为 {@code true}. + * + *
+	 * Assert.isAssignable(Number.class, myClass);
+	 * 
+ * + * @param superType 需要检查的父类或接口 + * @param subType 需要检查的子类 + * @throws IllegalArgumentException 如果子类非继承父类,抛出此异常 + */ + public static void isAssignable(Class superType, Class subType) throws IllegalArgumentException { + isAssignable(superType, subType, "{} is not assignable to {})", subType, superType); + } + + /** + * 断言 {@code superType.isAssignableFrom(subType)} 是否为 {@code true}. + * + *
+	 * Assert.isAssignable(Number.class, myClass, "myClass must can be assignable to class Number");
+	 * 
+ * + * @param superType 需要检查的父类或接口 + * @param subType 需要检查的子类 + * @param errorMsgTemplate 异常时的消息模板 + * @param params 参数列表 + * @throws IllegalArgumentException 如果子类非继承父类,抛出此异常 + */ + public static void isAssignable(Class superType, Class subType, String errorMsgTemplate, Object... params) throws IllegalArgumentException { + notNull(superType, "Type to check against must not be null"); + if (subType == null || !superType.isAssignableFrom(subType)) { + throw new IllegalArgumentException(StrUtil.format(errorMsgTemplate, params)); + } + } + + /** + * 检查boolean表达式,当检查结果为false时抛出 {@code IllegalStateException}。 + * 并使用指定的函数获取错误信息返回 + *
+	 * Assert.state(id == null, ()->{
+	 *      // to query relation message
+	 *      return "relation message to return ";
+	 *  });
+	 * 
+ * + * @param expression boolean 表达式 + * @param errorMsgSupplier 错误抛出异常附带的消息生产接口 + * @throws IllegalStateException 表达式为 {@code false} 抛出此异常 + */ + public static void state(boolean expression, Supplier errorMsgSupplier) throws IllegalStateException { + if (!expression) { + throw new IllegalStateException(errorMsgSupplier.get()); + } + } + + /** + * 检查boolean表达式,当检查结果为false时抛出 {@code IllegalStateException}。 + * + *
+	 * Assert.state(id == null, "The id property must not already be initialized");
+	 * 
+ * + * @param expression boolean 表达式 + * @param errorMsgTemplate 异常时的消息模板 + * @param params 参数列表 + * @throws IllegalStateException 表达式为 {@code false} 抛出此异常 + */ + public static void state(boolean expression, String errorMsgTemplate, Object... params) throws IllegalStateException { + if (!expression) { + throw new IllegalStateException(StrUtil.format(errorMsgTemplate, params)); + } + } + + /** + * 检查boolean表达式,当检查结果为false时抛出 {@code IllegalStateException}。 + * + *
+	 * Assert.state(id == null);
+	 * 
+ * + * @param expression boolean 表达式 + * @throws IllegalStateException 表达式为 {@code false} 抛出此异常 + */ + public static void state(boolean expression) throws IllegalStateException { + state(expression, "[Assertion failed] - this state invariant must be true"); + } + + /** + * 检查下标(数组、集合、字符串)是否符合要求,下标必须满足: + * + *
+	 * 0 ≤ index < size
+	 * 
+ * + * @param index 下标 + * @param size 长度 + * @return 检查后的下标 + * @throws IllegalArgumentException 如果size < 0 抛出此异常 + * @throws IndexOutOfBoundsException 如果index < 0或者 index ≥ size 抛出此异常 + * @since 4.1.9 + */ + public static int checkIndex(int index, int size) throws IllegalArgumentException, IndexOutOfBoundsException { + return checkIndex(index, size, "[Assertion failed]"); + } + + /** + * 检查下标(数组、集合、字符串)是否符合要求,下标必须满足: + * + *
+	 * 0 ≤ index < size
+	 * 
+ * + * @param index 下标 + * @param size 长度 + * @param errorMsgTemplate 异常时的消息模板 + * @param params 参数列表 + * @return 检查后的下标 + * @throws IllegalArgumentException 如果size < 0 抛出此异常 + * @throws IndexOutOfBoundsException 如果index < 0或者 index ≥ size 抛出此异常 + * @since 4.1.9 + */ + public static int checkIndex(int index, int size, String errorMsgTemplate, Object... params) throws IllegalArgumentException, IndexOutOfBoundsException { + if (index < 0 || index >= size) { + throw new IndexOutOfBoundsException(badIndexMsg(index, size, errorMsgTemplate, params)); + } + return index; + } + + /** + * 检查值是否在指定范围内 + * + * @param 异常类型 + * @param value 值 + * @param min 最小值(包含) + * @param max 最大值(包含) + * @param errorSupplier 错误抛出异常附带的消息生产接口 + * @return 经过检查后的值 + * @throws X if value is out of bound + * @since 5.7.15 + */ + public static int checkBetween(int value, int min, int max, Supplier errorSupplier) throws X { + if (value < min || value > max) { + throw errorSupplier.get(); + } + + return value; + } + + /** + * 检查值是否在指定范围内 + * + * @param value 值 + * @param min 最小值(包含) + * @param max 最大值(包含) + * @param errorMsgTemplate 异常信息模板,类似于"aa{}bb{}cc" + * @param params 异常信息参数,用于替换"{}"占位符 + * @return 经过检查后的值 + * @since 5.7.15 + */ + public static int checkBetween(int value, int min, int max, String errorMsgTemplate, Object... params) { + return checkBetween(value, min, max, () -> new IllegalArgumentException(StrUtil.format(errorMsgTemplate, params))); + } + + /** + * 检查值是否在指定范围内 + * + * @param value 值 + * @param min 最小值(包含) + * @param max 最大值(包含) + * @return 检查后的长度值 + * @since 4.1.10 + */ + public static int checkBetween(int value, int min, int max) { + return checkBetween(value, min, max, TEMPLATE_VALUE_MUST_BE_BETWEEN_AND, min, max); + } + + /** + * 检查值是否在指定范围内 + * + * @param 异常类型 + * @param value 值 + * @param min 最小值(包含) + * @param max 最大值(包含) + * @param errorSupplier 错误抛出异常附带的消息生产接口 + * @return 经过检查后的值 + * @throws X if value is out of bound + * @since 5.7.15 + */ + public static long checkBetween(long value, long min, long max, Supplier errorSupplier) throws X { + if (value < min || value > max) { + throw errorSupplier.get(); + } + + return value; + } + + /** + * 检查值是否在指定范围内 + * + * @param value 值 + * @param min 最小值(包含) + * @param max 最大值(包含) + * @param errorMsgTemplate 异常信息模板,类似于"aa{}bb{}cc" + * @param params 异常信息参数,用于替换"{}"占位符 + * @return 经过检查后的值 + * @since 5.7.15 + */ + public static long checkBetween(long value, long min, long max, String errorMsgTemplate, Object... params) { + return checkBetween(value, min, max, () -> new IllegalArgumentException(StrUtil.format(errorMsgTemplate, params))); + } + + /** + * 检查值是否在指定范围内 + * + * @param value 值 + * @param min 最小值(包含) + * @param max 最大值(包含) + * @return 检查后的长度值 + * @since 4.1.10 + */ + public static long checkBetween(long value, long min, long max) { + return checkBetween(value, min, max, TEMPLATE_VALUE_MUST_BE_BETWEEN_AND, min, max); + } + + /** + * 检查值是否在指定范围内 + * + * @param 异常类型 + * @param value 值 + * @param min 最小值(包含) + * @param max 最大值(包含) + * @param errorSupplier 错误抛出异常附带的消息生产接口 + * @return 经过检查后的值 + * @throws X if value is out of bound + * @since 5.7.15 + */ + public static double checkBetween(double value, double min, double max, Supplier errorSupplier) throws X { + if (value < min || value > max) { + throw errorSupplier.get(); + } + + return value; + } + + /** + * 检查值是否在指定范围内 + * + * @param value 值 + * @param min 最小值(包含) + * @param max 最大值(包含) + * @param errorMsgTemplate 异常信息模板,类似于"aa{}bb{}cc" + * @param params 异常信息参数,用于替换"{}"占位符 + * @return 经过检查后的值 + * @since 5.7.15 + */ + public static double checkBetween(double value, double min, double max, String errorMsgTemplate, Object... params) { + return checkBetween(value, min, max, () -> new IllegalArgumentException(StrUtil.format(errorMsgTemplate, params))); + } + + /** + * 检查值是否在指定范围内 + * + * @param value 值 + * @param min 最小值(包含) + * @param max 最大值(包含) + * @return 检查后的长度值 + * @since 4.1.10 + */ + public static double checkBetween(double value, double min, double max) { + return checkBetween(value, min, max, TEMPLATE_VALUE_MUST_BE_BETWEEN_AND, min, max); + } + + /** + * 检查值是否在指定范围内 + * + * @param value 值 + * @param min 最小值(包含) + * @param max 最大值(包含) + * @return 检查后的长度值 + * @since 4.1.10 + */ + public static Number checkBetween(Number value, Number min, Number max) { + notNull(value); + notNull(min); + notNull(max); + double valueDouble = value.doubleValue(); + double minDouble = min.doubleValue(); + double maxDouble = max.doubleValue(); + if (valueDouble < minDouble || valueDouble > maxDouble) { + throw new IllegalArgumentException(StrUtil.format(TEMPLATE_VALUE_MUST_BE_BETWEEN_AND, min, max)); + } + return value; + } + + /** + * 断言两个对象是否不相等,如果两个对象相等 抛出IllegalArgumentException 异常 + *
+	 *   Assert.notEquals(obj1,obj2);
+	 * 
+ * + * @param obj1 对象1 + * @param obj2 对象2 + * @throws IllegalArgumentException obj1 must be not equals obj2 + */ + public static void notEquals(Object obj1, Object obj2) { + notEquals(obj1, obj2, "({}) must be not equals ({})", obj1, obj2); + } + + /** + * 断言两个对象是否不相等,如果两个对象相等 抛出IllegalArgumentException 异常 + *
+	 *   Assert.notEquals(obj1,obj2,"obj1 must be not equals obj2");
+	 * 
+ * + * @param obj1 对象1 + * @param obj2 对象2 + * @param errorMsgTemplate 异常信息模板,类似于"aa{}bb{}cc" + * @param params 异常信息参数,用于替换"{}"占位符 + * @throws IllegalArgumentException obj1 must be not equals obj2 + */ + public static void notEquals(Object obj1, Object obj2, String errorMsgTemplate, Object... params) throws IllegalArgumentException { + notEquals(obj1, obj2, () -> new IllegalArgumentException(StrUtil.format(errorMsgTemplate, params))); + } + + /** + * 断言两个对象是否不相等,如果两个对象相等,抛出指定类型异常,并使用指定的函数获取错误信息返回 + * + * @param obj1 对象1 + * @param obj2 对象2 + * @param errorSupplier 错误抛出异常附带的消息生产接口 + * @param 异常类型 + * @throws X obj1 must be not equals obj2 + */ + public static void notEquals(Object obj1, Object obj2, Supplier errorSupplier) throws X { + if (ObjectUtil.equals(obj1, obj2)) { + throw errorSupplier.get(); + } + } + // ----------------------------------------------------------------------------------------------------------- Check not equals + + /** + * 断言两个对象是否相等,如果两个对象不相等 抛出IllegalArgumentException 异常 + *
+	 *   Assert.isEquals(obj1,obj2);
+	 * 
+ * + * @param obj1 对象1 + * @param obj2 对象2 + * @throws IllegalArgumentException obj1 must be equals obj2 + */ + public static void equals(Object obj1, Object obj2) { + equals(obj1, obj2, "({}) must be equals ({})", obj1, obj2); + } + + /** + * 断言两个对象是否相等,如果两个对象不相等 抛出IllegalArgumentException 异常 + *
+	 *   Assert.isEquals(obj1,obj2,"obj1 must be equals obj2");
+	 * 
+ * + * @param obj1 对象1 + * @param obj2 对象2 + * @param errorMsgTemplate 异常信息模板,类似于"aa{}bb{}cc" + * @param params 异常信息参数,用于替换"{}"占位符 + * @throws IllegalArgumentException obj1 must be equals obj2 + */ + public static void equals(Object obj1, Object obj2, String errorMsgTemplate, Object... params) throws IllegalArgumentException { + equals(obj1, obj2, () -> new IllegalArgumentException(StrUtil.format(errorMsgTemplate, params))); + } + + /** + * 断言两个对象是否相等,如果两个对象不相等,抛出指定类型异常,并使用指定的函数获取错误信息返回 + * + * @param obj1 对象1 + * @param obj2 对象2 + * @param errorSupplier 错误抛出异常附带的消息生产接口 + * @param 异常类型 + * @throws X obj1 must be equals obj2 + */ + public static void equals(Object obj1, Object obj2, Supplier errorSupplier) throws X { + if (ObjectUtil.notEqual(obj1, obj2)) { + throw errorSupplier.get(); + } + } + + // ----------------------------------------------------------------------------------------------------------- Check is equals + + // -------------------------------------------------------------------------------------------------------------------------------------------- Private method start + + /** + * 错误的下标时显示的消息 + * + * @param index 下标 + * @param size 长度 + * @param desc 异常时的消息模板 + * @param params 参数列表 + * @return 消息 + */ + private static String badIndexMsg(int index, int size, String desc, Object... params) { + if (index < 0) { + return StrUtil.format("{} ({}) must not be negative", StrUtil.format(desc, params), index); + } else if (size < 0) { + throw new IllegalArgumentException("negative size: " + size); + } else { // index >= size + return StrUtil.format("{} ({}) must be less than size ({})", StrUtil.format(desc, params), index, size); + } + } + // -------------------------------------------------------------------------------------------------------------------------------------------- Private method end +} diff --git a/src/main/java/cn/hutool/core/lang/Chain.java b/src/main/java/cn/hutool/core/lang/Chain.java new file mode 100644 index 0000000..536380c --- /dev/null +++ b/src/main/java/cn/hutool/core/lang/Chain.java @@ -0,0 +1,17 @@ +package cn.hutool.core.lang; + +/** + * 责任链接口 + * @author Looly + * + * @param 元素类型 + * @param 目标类类型,用于返回this对象 + */ +public interface Chain extends Iterable{ + /** + * 加入责任链 + * @param element 责任链新的环节元素 + * @return this + */ + T addChain(E element); +} diff --git a/src/main/java/cn/hutool/core/lang/ClassScanner.java b/src/main/java/cn/hutool/core/lang/ClassScanner.java new file mode 100644 index 0000000..f74e900 --- /dev/null +++ b/src/main/java/cn/hutool/core/lang/ClassScanner.java @@ -0,0 +1,460 @@ +package cn.hutool.core.lang; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.collection.EnumerationIter; +import cn.hutool.core.io.FileUtil; +import cn.hutool.core.io.IORuntimeException; +import cn.hutool.core.io.resource.ResourceUtil; +import cn.hutool.core.util.*; + +import java.io.File; +import java.io.IOException; +import java.io.Serializable; +import java.lang.annotation.Annotation; +import java.net.URL; +import java.nio.charset.Charset; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; + +/** + * 类扫描器 + * + * @author looly + * @since 4.6.9 + */ +public class ClassScanner implements Serializable { + private static final long serialVersionUID = 1L; + + /** + * 包名 + */ + private final String packageName; + /** + * 包名,最后跟一个点,表示包名,避免在检查前缀时的歧义
+ * 如果包名指定为空,不跟点 + */ + private final String packageNameWithDot; + /** + * 包路径,用于文件中对路径操作 + */ + private final String packageDirName; + /** + * 包路径,用于jar中对路径操作,在Linux下与packageDirName一致 + */ + private final String packagePath; + /** + * 过滤器 + */ + private final Filter> classFilter; + /** + * 编码 + */ + private final Charset charset; + /** + * 类加载器 + */ + private ClassLoader classLoader; + /** + * 是否初始化类 + */ + private boolean initialize; + /** + * 扫描结果集 + */ + private final Set> classes = new HashSet<>(); + + /** + * 忽略loadClass时的错误 + */ + private boolean ignoreLoadError = false; + + /** + * 获取加载错误的类名列表 + */ + private final Set classesOfLoadError = new HashSet<>(); + + /** + * 扫描指定包路径下所有包含指定注解的类,包括其他加载的jar或者类 + * + * @param packageName 包路径 + * @param annotationClass 注解类 + * @return 类集合 + */ + public static Set> scanAllPackageByAnnotation(String packageName, Class annotationClass) { + return scanAllPackage(packageName, clazz -> clazz.isAnnotationPresent(annotationClass)); + } + + /** + * 扫描指定包路径下所有包含指定注解的类
+ * 如果classpath下已经有类,不再扫描其他加载的jar或者类 + * + * @param packageName 包路径 + * @param annotationClass 注解类 + * @return 类集合 + */ + public static Set> scanPackageByAnnotation(String packageName, Class annotationClass) { + return scanPackage(packageName, clazz -> clazz.isAnnotationPresent(annotationClass)); + } + + /** + * 扫描指定包路径下所有指定类或接口的子类或实现类,不包括指定父类本身,包括其他加载的jar或者类 + * + * @param packageName 包路径 + * @param superClass 父类或接口(不包括) + * @return 类集合 + */ + public static Set> scanAllPackageBySuper(String packageName, Class superClass) { + return scanAllPackage(packageName, clazz -> superClass.isAssignableFrom(clazz) && !superClass.equals(clazz)); + } + + /** + * 扫描指定包路径下所有指定类或接口的子类或实现类,不包括指定父类本身
+ * 如果classpath下已经有类,不再扫描其他加载的jar或者类 + * + * @param packageName 包路径 + * @param superClass 父类或接口(不包括) + * @return 类集合 + */ + public static Set> scanPackageBySuper(String packageName, Class superClass) { + return scanPackage(packageName, clazz -> superClass.isAssignableFrom(clazz) && !superClass.equals(clazz)); + } + + /** + * 扫描该包路径下所有class文件,包括其他加载的jar或者类 + * + * @return 类集合 + * @since 5.7.5 + */ + public static Set> scanAllPackage() { + return scanAllPackage(StrUtil.EMPTY, null); + } + + /** + * 扫描classpath下所有class文件,如果classpath下已经有类,不再扫描其他加载的jar或者类 + * + * @return 类集合 + */ + public static Set> scanPackage() { + return scanPackage(StrUtil.EMPTY, null); + } + + /** + * 扫描该包路径下所有class文件 + * + * @param packageName 包路径 com | com. | com.abs | com.abs. + * @return 类集合 + */ + public static Set> scanPackage(String packageName) { + return scanPackage(packageName, null); + } + + /** + * 扫描包路径下和所有在classpath中加载的类,满足class过滤器条件的所有class文件,
+ * 如果包路径为 com.abs + A.class 但是输入 abs会产生classNotFoundException
+ * 因为className 应该为 com.abs.A 现在却成为abs.A,此工具类对该异常进行忽略处理
+ * + * @param packageName 包路径 com | com. | com.abs | com.abs. + * @param classFilter class过滤器,过滤掉不需要的class + * @return 类集合 + * @since 5.7.5 + */ + public static Set> scanAllPackage(String packageName, Filter> classFilter) { + return new ClassScanner(packageName, classFilter).scan(true); + } + + /** + * 扫描包路径下满足class过滤器条件的所有class文件,
+ * 如果包路径为 com.abs + A.class 但是输入 abs会产生classNotFoundException
+ * 因为className 应该为 com.abs.A 现在却成为abs.A,此工具类对该异常进行忽略处理
+ * + * @param packageName 包路径 com | com. | com.abs | com.abs. + * @param classFilter class过滤器,过滤掉不需要的class + * @return 类集合 + */ + public static Set> scanPackage(String packageName, Filter> classFilter) { + return new ClassScanner(packageName, classFilter).scan(); + } + + /** + * 构造,默认UTF-8编码 + */ + public ClassScanner() { + this(null); + } + + /** + * 构造,默认UTF-8编码 + * + * @param packageName 包名,所有包传入""或者null + */ + public ClassScanner(String packageName) { + this(packageName, null); + } + + /** + * 构造,默认UTF-8编码 + * + * @param packageName 包名,所有包传入""或者null + * @param classFilter 过滤器,无需传入null + */ + public ClassScanner(String packageName, Filter> classFilter) { + this(packageName, classFilter, CharsetUtil.CHARSET_UTF_8); + } + + /** + * 构造 + * + * @param packageName 包名,所有包传入""或者null + * @param classFilter 过滤器,无需传入null + * @param charset 编码 + */ + public ClassScanner(String packageName, Filter> classFilter, Charset charset) { + packageName = StrUtil.nullToEmpty(packageName); + this.packageName = packageName; + this.packageNameWithDot = StrUtil.addSuffixIfNot(packageName, StrUtil.DOT); + this.packageDirName = packageName.replace(CharUtil.DOT, File.separatorChar); + this.packagePath = packageName.replace(CharUtil.DOT, CharUtil.SLASH); + this.classFilter = classFilter; + this.charset = charset; + } + + /** + * 设置是否忽略loadClass时的错误 + * + * @param ignoreLoadError 忽略loadClass时的错误 + * @return this + */ + public ClassScanner setIgnoreLoadError(boolean ignoreLoadError) { + this.ignoreLoadError = ignoreLoadError; + return this; + } + + /** + * 扫描包路径下满足class过滤器条件的所有class文件
+ * 此方法首先扫描指定包名下的资源目录,如果未扫描到,则扫描整个classpath中所有加载的类 + * + * @return 类集合 + */ + public Set> scan() { + return scan(false); + } + + /** + * 扫描包路径下满足class过滤器条件的所有class文件 + * + * @param forceScanJavaClassPaths 是否强制扫描其他位于classpath关联jar中的类 + * @return 类集合 + * @since 5.7.5 + */ + public Set> scan(boolean forceScanJavaClassPaths) { + + //多次扫描时,清理上次扫描历史 + this.classes.clear(); + this.classesOfLoadError.clear(); + + for (URL url : ResourceUtil.getResourceIter(this.packagePath, this.classLoader)) { + switch (url.getProtocol()) { + case "file": + scanFile(new File(URLUtil.decode(url.getFile(), this.charset.name())), null); + break; + case "jar": + scanJar(URLUtil.getJarFile(url)); + break; + } + } + + // classpath下未找到,则扫描其他jar包下的类 + if (forceScanJavaClassPaths || CollUtil.isEmpty(this.classes)) { + scanJavaClassPaths(); + } + + return Collections.unmodifiableSet(this.classes); + } + + /** + * 设置是否在扫描到类时初始化类 + * + * @param initialize 是否初始化类 + */ + public void setInitialize(boolean initialize) { + this.initialize = initialize; + } + + /** + * 设置自定义的类加载器 + * + * @param classLoader 类加载器 + * @since 4.6.9 + */ + public void setClassLoader(ClassLoader classLoader) { + this.classLoader = classLoader; + } + + /** + * 忽略加载错误扫描后,可以获得之前扫描时加载错误的类名字集合 + */ + public Set getClassesOfLoadError() { + return Collections.unmodifiableSet(this.classesOfLoadError); + } + + // --------------------------------------------------------------------------------------------------- Private method start + + @Override + protected Object clone() throws CloneNotSupportedException { + return super.clone(); + } + + /** + * 扫描Java指定的ClassPath路径 + */ + private void scanJavaClassPaths() { + final String[] javaClassPaths = ClassUtil.getJavaClassPaths(); + for (String classPath : javaClassPaths) { + // bug修复,由于路径中空格和中文导致的Jar找不到 + classPath = URLUtil.decode(classPath, CharsetUtil.systemCharsetName()); + + scanFile(new File(classPath), null); + } + } + + /** + * 扫描文件或目录中的类 + * + * @param file 文件或目录 + * @param rootDir 包名对应classpath绝对路径 + */ + private void scanFile(File file, String rootDir) { + if (file.isFile()) { + final String fileName = file.getAbsolutePath(); + if (fileName.endsWith(FileUtil.CLASS_EXT)) { + final String className = fileName// + // 8为classes长度,fileName.length() - 6为".class"的长度 + .substring(rootDir.length(), fileName.length() - 6)// + .replace(File.separatorChar, CharUtil.DOT);// + //加入满足条件的类 + addIfAccept(className); + } else if (fileName.endsWith(FileUtil.JAR_FILE_EXT)) { + try { + scanJar(new JarFile(file)); + } catch (IOException e) { + throw new IORuntimeException(e); + } + } + } else if (file.isDirectory()) { + final File[] files = file.listFiles(); + if (null != files) { + for (File subFile : files) { + scanFile(subFile, (null == rootDir) ? subPathBeforePackage(file) : rootDir); + } + } + } + } + + /** + * 扫描jar包 + * + * @param jar jar包 + */ + private void scanJar(JarFile jar) { + String name; + for (JarEntry entry : new EnumerationIter<>(jar.entries())) { + name = StrUtil.removePrefix(entry.getName(), StrUtil.SLASH); + if (StrUtil.isEmpty(packagePath) || name.startsWith(this.packagePath)) { + if (name.endsWith(FileUtil.CLASS_EXT) && !entry.isDirectory()) { + final String className = name// + .substring(0, name.length() - 6)// + .replace(CharUtil.SLASH, CharUtil.DOT);// + addIfAccept(loadClass(className)); + } + } + } + } + + /** + * 加载类 + * + * @param className 类名 + * @return 加载的类 + */ + protected Class loadClass(String className) { + ClassLoader loader = this.classLoader; + if (null == loader) { + loader = ClassLoaderUtil.getClassLoader(); + this.classLoader = loader; + } + + Class clazz = null; + try { + clazz = Class.forName(className, this.initialize, loader); + } catch (NoClassDefFoundError | ClassNotFoundException e) { + // 由于依赖库导致的类无法加载,直接跳过此类 + classesOfLoadError.add(className); + } catch (UnsupportedClassVersionError e) { + // 版本导致的不兼容的类,跳过 + classesOfLoadError.add(className); + } catch (Throwable e) { + if (!this.ignoreLoadError) { + throw new RuntimeException(e); + } else { + classesOfLoadError.add(className); + } + } + return clazz; + } + + /** + * 通过过滤器,是否满足接受此类的条件 + * + * @param className 类名 + */ + private void addIfAccept(String className) { + if (StrUtil.isBlank(className)) { + return; + } + int classLen = className.length(); + int packageLen = this.packageName.length(); + if (classLen == packageLen) { + //类名和包名长度一致,用户可能传入的包名是类名 + if (className.equals(this.packageName)) { + addIfAccept(loadClass(className)); + } + } else if (classLen > packageLen) { + //检查类名是否以指定包名为前缀,包名后加.(避免类似于cn.hutool.A和cn.hutool.ATest这类类名引起的歧义) + if (".".equals(this.packageNameWithDot) || className.startsWith(this.packageNameWithDot)) { + addIfAccept(loadClass(className)); + } + } + } + + /** + * 通过过滤器,是否满足接受此类的条件 + * + * @param clazz 类 + */ + private void addIfAccept(Class clazz) { + if (null != clazz) { + Filter> classFilter = this.classFilter; + if (classFilter == null || classFilter.accept(clazz)) { + this.classes.add(clazz); + } + } + } + + /** + * 截取文件绝对路径中包名之前的部分 + * + * @param file 文件 + * @return 包名之前的部分 + */ + private String subPathBeforePackage(File file) { + String filePath = file.getAbsolutePath(); + if (StrUtil.isNotEmpty(this.packageDirName)) { + filePath = StrUtil.subBefore(filePath, this.packageDirName, true); + } + return StrUtil.addSuffixIfNot(filePath, File.separator); + } + // --------------------------------------------------------------------------------------------------- Private method end +} diff --git a/src/main/java/cn/hutool/core/lang/ConsistentHash.java b/src/main/java/cn/hutool/core/lang/ConsistentHash.java new file mode 100644 index 0000000..feff9e0 --- /dev/null +++ b/src/main/java/cn/hutool/core/lang/ConsistentHash.java @@ -0,0 +1,101 @@ +package cn.hutool.core.lang; + +import cn.hutool.core.lang.hash.Hash32; +import cn.hutool.core.util.HashUtil; + +import java.io.Serializable; +import java.util.Collection; +import java.util.SortedMap; +import java.util.TreeMap; + +/** + * 一致性Hash算法 + * 算法详解:http://blog.csdn.net/sparkliang/article/details/5279393 + * 算法实现:https://weblogs.java.net/blog/2007/11/27/consistent-hashing + * @author xiaoleilu + * + * @param 节点类型 + */ +public class ConsistentHash implements Serializable{ + private static final long serialVersionUID = 1L; + + /** Hash计算对象,用于自定义hash算法 */ + Hash32 hashFunc; + /** 复制的节点个数 */ + private final int numberOfReplicas; + /** 一致性Hash环 */ + private final SortedMap circle = new TreeMap<>(); + + /** + * 构造,使用Java默认的Hash算法 + * @param numberOfReplicas 复制的节点个数,增加每个节点的复制节点有利于负载均衡 + * @param nodes 节点对象 + */ + public ConsistentHash(int numberOfReplicas, Collection nodes) { + this.numberOfReplicas = numberOfReplicas; + this.hashFunc = key -> { + //默认使用FNV1hash算法 + return HashUtil.fnvHash(key.toString()); + }; + //初始化节点 + for (T node : nodes) { + add(node); + } + } + + /** + * 构造 + * @param hashFunc hash算法对象 + * @param numberOfReplicas 复制的节点个数,增加每个节点的复制节点有利于负载均衡 + * @param nodes 节点对象 + */ + public ConsistentHash(Hash32 hashFunc, int numberOfReplicas, Collection nodes) { + this.numberOfReplicas = numberOfReplicas; + this.hashFunc = hashFunc; + //初始化节点 + for (T node : nodes) { + add(node); + } + } + + /** + * 增加节点
+ * 每增加一个节点,就会在闭环上增加给定复制节点数
+ * 例如复制节点数是2,则每调用此方法一次,增加两个虚拟节点,这两个节点指向同一Node + * 由于hash算法会调用node的toString方法,故按照toString去重 + * @param node 节点对象 + */ + public void add(T node) { + for (int i = 0; i < numberOfReplicas; i++) { + circle.put(hashFunc.hash32(node.toString() + i), node); + } + } + + /** + * 移除节点的同时移除相应的虚拟节点 + * @param node 节点对象 + */ + public void remove(T node) { + for (int i = 0; i < numberOfReplicas; i++) { + circle.remove(hashFunc.hash32(node.toString() + i)); + } + } + + /** + * 获得一个最近的顺时针节点 + * @param key 为给定键取Hash,取得顺时针方向上最近的一个虚拟节点对应的实际节点 + * @return 节点对象 + */ + public T get(Object key) { + if (circle.isEmpty()) { + return null; + } + int hash = hashFunc.hash32(key); + if (!circle.containsKey(hash)) { + SortedMap tailMap = circle.tailMap(hash); //返回此映射的部分视图,其键大于等于 hash + hash = tailMap.isEmpty() ? circle.firstKey() : tailMap.firstKey(); + } + //正好命中 + return circle.get(hash); + } +} \ No newline at end of file diff --git a/src/main/java/cn/hutool/core/lang/Console.java b/src/main/java/cn/hutool/core/lang/Console.java new file mode 100644 index 0000000..643f9cb --- /dev/null +++ b/src/main/java/cn/hutool/core/lang/Console.java @@ -0,0 +1,331 @@ +package cn.hutool.core.lang; + +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.CharUtil; +import cn.hutool.core.util.StrUtil; + +import java.util.Scanner; + +import static java.lang.System.err; +import static java.lang.System.out; + +/** + * 命令行(控制台)工具方法类
+ * 此类主要针对{@link System#out} 和 {@link System#err} 做封装。 + * + * @author Looly + */ + +public class Console { + + private static final String TEMPLATE_VAR = "{}"; + + // --------------------------------------------------------------------------------- Log + + /** + * 同 System.out.println()方法,打印控制台日志 + */ + public static void log() { + out.println(); + } + + /** + * 同 System.out.println()方法,打印控制台日志
+ * 如果传入打印对象为{@link Throwable}对象,那么同时打印堆栈 + * + * @param obj 要打印的对象 + */ + public static void log(Object obj) { + if (obj instanceof Throwable) { + final Throwable e = (Throwable) obj; + log(e, e.getMessage()); + } else { + log(TEMPLATE_VAR, obj); + } + } + + /** + * 同 System.out.println()方法,打印控制台日志
+ * 如果传入打印对象为{@link Throwable}对象,那么同时打印堆栈 + * + * @param obj1 第一个要打印的对象 + * @param otherObjs 其它要打印的对象 + * @since 5.4.3 + */ + public static void log(Object obj1, Object... otherObjs) { + if (ArrayUtil.isEmpty(otherObjs)) { + log(obj1); + } else { + log(buildTemplateSplitBySpace(otherObjs.length + 1), ArrayUtil.insert(otherObjs, 0, obj1)); + } + } + + /** + * 同 System.out.println()方法,打印控制台日志
+ * 当传入template无"{}"时,被认为非模板,直接打印多个参数以空格分隔 + * + * @param template 文本模板,被替换的部分用 {} 表示 + * @param values 值 + */ + public static void log(String template, Object... values) { + if (ArrayUtil.isEmpty(values) || StrUtil.contains(template, TEMPLATE_VAR)) { + logInternal(template, values); + } else { + logInternal(buildTemplateSplitBySpace(values.length + 1), ArrayUtil.insert(values, 0, template)); + } + } + + /** + * 同 System.out.println()方法,打印控制台日志 + * + * @param t 异常对象 + * @param template 文本模板,被替换的部分用 {} 表示 + * @param values 值 + */ + public static void log(Throwable t, String template, Object... values) { + out.println(StrUtil.format(template, values)); + if (null != t) { + //noinspection CallToPrintStackTrace + t.printStackTrace(out); + out.flush(); + } + } + + /** + * 同 System.out.println()方法,打印控制台日志 + * + * @param template 文本模板,被替换的部分用 {} 表示 + * @param values 值 + * @since 5.4.3 + */ + private static void logInternal(String template, Object... values) { + log(null, template, values); + } + + // --------------------------------------------------------------------------------- print + + /** + * 打印表格到控制台 + * + * @param consoleTable 控制台表格 + * @since 5.4.5 + */ + public static void table(ConsoleTable consoleTable) { + print(consoleTable.toString()); + } + + /** + * 同 System.out.print()方法,打印控制台日志 + * + * @param obj 要打印的对象 + * @since 3.3.1 + */ + public static void print(Object obj) { + print(TEMPLATE_VAR, obj); + } + + /** + * 同 System.out.println()方法,打印控制台日志
+ * 如果传入打印对象为{@link Throwable}对象,那么同时打印堆栈 + * + * @param obj1 第一个要打印的对象 + * @param otherObjs 其它要打印的对象 + * @since 5.4.3 + */ + public static void print(Object obj1, Object... otherObjs) { + if (ArrayUtil.isEmpty(otherObjs)) { + print(obj1); + } else { + print(buildTemplateSplitBySpace(otherObjs.length + 1), ArrayUtil.insert(otherObjs, 0, obj1)); + } + } + + /** + * 同 System.out.print()方法,打印控制台日志 + * + * @param template 文本模板,被替换的部分用 {} 表示 + * @param values 值 + * @since 3.3.1 + */ + public static void print(String template, Object... values) { + if (ArrayUtil.isEmpty(values) || StrUtil.contains(template, TEMPLATE_VAR)) { + printInternal(template, values); + } else { + printInternal(buildTemplateSplitBySpace(values.length + 1), ArrayUtil.insert(values, 0, template)); + } + } + + /** + * 打印进度条 + * + * @param showChar 进度条提示字符,例如“#” + * @param len 打印长度 + * @since 4.5.6 + */ + public static void printProgress(char showChar, int len) { + print("{}{}", CharUtil.CR, StrUtil.repeat(showChar, len)); + } + + /** + * 打印进度条 + * + * @param showChar 进度条提示字符,例如“#” + * @param totalLen 总长度 + * @param rate 总长度所占比取值0~1 + * @since 4.5.6 + */ + public static void printProgress(char showChar, int totalLen, double rate) { + Assert.isTrue(rate >= 0 && rate <= 1, "Rate must between 0 and 1 (both include)"); + printProgress(showChar, (int) (totalLen * rate)); + } + + /** + * 同 System.out.println()方法,打印控制台日志 + * + * @param template 文本模板,被替换的部分用 {} 表示 + * @param values 值 + * @since 5.4.3 + */ + private static void printInternal(String template, Object... values) { + out.print(StrUtil.format(template, values)); + } + + // --------------------------------------------------------------------------------- Error + + /** + * 同 System.err.println()方法,打印控制台日志 + */ + public static void error() { + err.println(); + } + + /** + * 同 System.err.println()方法,打印控制台日志 + * + * @param obj 要打印的对象 + */ + public static void error(Object obj) { + if (obj instanceof Throwable) { + Throwable e = (Throwable) obj; + error(e, e.getMessage()); + } else { + error(TEMPLATE_VAR, obj); + } + } + + /** + * 同 System.out.println()方法,打印控制台日志
+ * 如果传入打印对象为{@link Throwable}对象,那么同时打印堆栈 + * + * @param obj1 第一个要打印的对象 + * @param otherObjs 其它要打印的对象 + * @since 5.4.3 + */ + public static void error(Object obj1, Object... otherObjs) { + if (ArrayUtil.isEmpty(otherObjs)) { + error(obj1); + } else { + error(buildTemplateSplitBySpace(otherObjs.length + 1), ArrayUtil.insert(otherObjs, 0, obj1)); + } + } + + /** + * 同 System.err.println()方法,打印控制台日志 + * + * @param template 文本模板,被替换的部分用 {} 表示 + * @param values 值 + */ + public static void error(String template, Object... values) { + if (ArrayUtil.isEmpty(values) || StrUtil.contains(template, TEMPLATE_VAR)) { + errorInternal(template, values); + } else { + errorInternal(buildTemplateSplitBySpace(values.length + 1), ArrayUtil.insert(values, 0, template)); + } + } + + /** + * 同 System.err.println()方法,打印控制台日志 + * + * @param t 异常对象 + * @param template 文本模板,被替换的部分用 {} 表示 + * @param values 值 + */ + public static void error(Throwable t, String template, Object... values) { + err.println(StrUtil.format(template, values)); + if (null != t) { + t.printStackTrace(err); + err.flush(); + } + } + + /** + * 同 System.err.println()方法,打印控制台日志 + * + * @param template 文本模板,被替换的部分用 {} 表示 + * @param values 值 + */ + private static void errorInternal(String template, Object... values) { + error(null, template, values); + } + + // --------------------------------------------------------------------------------- in + + /** + * 创建从控制台读取内容的{@link Scanner} + * + * @return {@link Scanner} + * @since 3.3.1 + */ + public static Scanner scanner() { + return new Scanner(System.in); + } + + /** + * 读取用户输入的内容(在控制台敲回车前的内容) + * + * @return 用户输入的内容 + * @since 3.3.1 + */ + public static String input() { + return scanner().nextLine(); + } + + // --------------------------------------------------------------------------------- console lineNumber + + /** + * 返回当前位置+行号 (不支持Lambda、内部类、递归内使用) + * + * @return 返回当前行号 + * @author dahuoyzs + * @since 5.2.5 + */ + public static String where() { + final StackTraceElement stackTraceElement = new Throwable().getStackTrace()[1]; + final String className = stackTraceElement.getClassName(); + final String methodName = stackTraceElement.getMethodName(); + final String fileName = stackTraceElement.getFileName(); + final Integer lineNumber = stackTraceElement.getLineNumber(); + return String.format("%s.%s(%s:%s)", className, methodName, fileName, lineNumber); + } + + /** + * 返回当前行号 (不支持Lambda、内部类、递归内使用) + * + * @return 返回当前行号 + * @since 5.2.5 + */ + public static Integer lineNumber() { + return new Throwable().getStackTrace()[1].getLineNumber(); + } + + /** + * 构建空格分隔的模板,类似于"{} {} {} {}" + * + * @param count 变量数量 + * @return 模板 + */ + private static String buildTemplateSplitBySpace(int count) { + return StrUtil.repeatAndJoin(TEMPLATE_VAR, count, StrUtil.SPACE); + } + +} diff --git a/src/main/java/cn/hutool/core/lang/ConsoleTable.java b/src/main/java/cn/hutool/core/lang/ConsoleTable.java new file mode 100644 index 0000000..380a839 --- /dev/null +++ b/src/main/java/cn/hutool/core/lang/ConsoleTable.java @@ -0,0 +1,206 @@ +package cn.hutool.core.lang; + +import cn.hutool.core.convert.Convert; +import cn.hutool.core.util.CharUtil; +import cn.hutool.core.util.StrUtil; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * 控制台打印表格工具 + * + * @author 孙宇 + * @since 5.4.4 + */ +public class ConsoleTable { + + private static final char ROW_LINE = '-'; + private static final char COLUMN_LINE = '|'; + private static final char CORNER = '+'; + private static final char SPACE = '\u3000'; + private static final char LF = CharUtil.LF; + + private boolean isSBCMode = true; + + /** + * 创建ConsoleTable对象 + * + * @return ConsoleTable + * @since 5.4.5 + */ + public static ConsoleTable create() { + return new ConsoleTable(); + } + + /** + * 表格头信息 + */ + private final List> headerList = new ArrayList<>(); + /** + * 表格体信息 + */ + private final List> bodyList = new ArrayList<>(); + /** + * 每列最大字符个数 + */ + private List columnCharNumber; + + /** + * 设置是否使用全角模式
+ * 当包含中文字符时,输出的表格可能无法对齐,因此当设置为全角模式时,全部字符转为全角。 + * + * @param isSBCMode 是否全角模式 + * @return this + * @since 5.8.0 + */ + public ConsoleTable setSBCMode(boolean isSBCMode) { + this.isSBCMode = isSBCMode; + return this; + } + + /** + * 添加头信息 + * + * @param titles 列名 + * @return 自身对象 + */ + public ConsoleTable addHeader(String... titles) { + if (columnCharNumber == null) { + columnCharNumber = new ArrayList<>(Collections.nCopies(titles.length, 0)); + } + List l = new ArrayList<>(); + fillColumns(l, titles); + headerList.add(l); + return this; + } + + /** + * 添加体信息 + * + * @param values 列值 + * @return 自身对象 + */ + public ConsoleTable addBody(String... values) { + List l = new ArrayList<>(); + bodyList.add(l); + fillColumns(l, values); + return this; + } + + /** + * 填充表格头或者体 + * + * @param l 被填充列表 + * @param columns 填充内容 + */ + private void fillColumns(List l, String[] columns) { + for (int i = 0; i < columns.length; i++) { + String column = columns[i]; + if (isSBCMode) { + column = Convert.toSBC(column); + } + l.add(column); + int width = column.length(); + if (width > columnCharNumber.get(i)) { + columnCharNumber.set(i, width); + } + } + } + + /** + * 获取表格字符串 + * + * @return 表格字符串 + */ + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + fillBorder(sb); + fillRows(sb, headerList); + fillBorder(sb); + fillRows(sb, bodyList); + fillBorder(sb); + return sb.toString(); + } + + /** + * 填充表头或者表体信息(多行) + * + * @param sb 内容 + * @param list 表头列表或者表体列表 + */ + private void fillRows(StringBuilder sb, List> list) { + for (List row : list) { + sb.append(COLUMN_LINE); + fillRow(sb, row); + sb.append(LF); + } + } + + /** + * 填充一行数据 + * + * @param sb 内容 + * @param row 一行数据 + */ + private void fillRow(StringBuilder sb, List row) { + final int size = row.size(); + String value; + for (int i = 0; i < size; i++) { + value = row.get(i); + sb.append(SPACE); + sb.append(value); + final int length = value.length(); + final int sbcCount = sbcCount(value); + if(sbcCount % 2 == 1){ + sb.append(CharUtil.SPACE); + } + sb.append(SPACE); + int maxLength = columnCharNumber.get(i); + for (int j = 0; j < (maxLength - length + (sbcCount / 2)); j++) { + sb.append(SPACE); + } + sb.append(COLUMN_LINE); + } + } + + /** + * 拼装边框 + * + * @param sb StringBuilder + */ + private void fillBorder(StringBuilder sb) { + sb.append(CORNER); + for (Integer width : columnCharNumber) { + sb.append(StrUtil.repeat(ROW_LINE, width + 2)); + sb.append(CORNER); + } + sb.append(LF); + } + + /** + * 打印到控制台 + */ + public void print() { + Console.print(toString()); + } + + /** + * 半角字符数量 + * + * @param value 字符串 + * @return 填充空格数量 + */ + private int sbcCount(String value) { + int count = 0; + for (int i = 0; i < value.length(); i++) { + if (value.charAt(i) < '\177') { + count++; + } + } + + return count; + } +} diff --git a/src/main/java/cn/hutool/core/lang/DefaultSegment.java b/src/main/java/cn/hutool/core/lang/DefaultSegment.java new file mode 100644 index 0000000..de5038c --- /dev/null +++ b/src/main/java/cn/hutool/core/lang/DefaultSegment.java @@ -0,0 +1,34 @@ +package cn.hutool.core.lang; + +/** + * 片段默认实现 + * + * @param 数字类型,用于表示位置index + * @author looly + * @since 5.5.3 + */ +public class DefaultSegment implements Segment { + + protected T startIndex; + protected T endIndex; + + /** + * 构造 + * @param startIndex 起始位置 + * @param endIndex 结束位置 + */ + public DefaultSegment(T startIndex, T endIndex) { + this.startIndex = startIndex; + this.endIndex = endIndex; + } + + @Override + public T getStartIndex() { + return this.startIndex; + } + + @Override + public T getEndIndex() { + return this.endIndex; + } +} diff --git a/src/main/java/cn/hutool/core/lang/Dict.java b/src/main/java/cn/hutool/core/lang/Dict.java new file mode 100644 index 0000000..c0b4c11 --- /dev/null +++ b/src/main/java/cn/hutool/core/lang/Dict.java @@ -0,0 +1,663 @@ +package cn.hutool.core.lang; + +import cn.hutool.core.bean.BeanPath; +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.convert.Convert; +import cn.hutool.core.getter.BasicTypeGetter; +import cn.hutool.core.lang.func.Func0; +import cn.hutool.core.lang.func.LambdaUtil; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.Arrays; +import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Objects; +import java.util.function.BiFunction; +import java.util.function.Function; + +/** + * 字典对象,扩充了HashMap中的方法 + * + * @author loolly + */ +public class Dict extends LinkedHashMap implements BasicTypeGetter { + private static final long serialVersionUID = 6135423866861206530L; + + static final float DEFAULT_LOAD_FACTOR = 0.75f; + static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16 + + /** + * 是否大小写不敏感 + */ + private boolean caseInsensitive; + + // --------------------------------------------------------------- Static method start + + /** + * 创建Dict + * + * @return Dict + */ + public static Dict create() { + return new Dict(); + } + + /** + * 将PO对象转为Dict + * + * @param Bean类型 + * @param bean Bean对象 + * @return Vo + */ + public static Dict parse(T bean) { + return create().parseBean(bean); + } + + /** + * 根据给定的Pair数组创建Dict对象 + * + * @param pairs 键值对 + * @return Dict + * @since 5.4.1 + */ + @SafeVarargs + public static Dict of(Pair... pairs) { + final Dict dict = create(); + for (Pair pair : pairs) { + dict.put(pair.getKey(), pair.getValue()); + } + return dict; + } + + /** + * 根据给定的键值对数组创建Dict对象,传入参数必须为key,value,key,value... + * + *

奇数参数必须为key,key最后会转换为String类型。

+ *

偶数参数必须为value,可以为任意类型。

+ * + *
+	 * Dict dict = Dict.of(
+	 * 	"RED", "#FF0000",
+	 * 	"GREEN", "#00FF00",
+	 * 	"BLUE", "#0000FF"
+	 * );
+	 * 
+ * + * @param keysAndValues 键值对列表,必须奇数参数为key,偶数参数为value + * @return Dict + * @since 5.4.1 + */ + public static Dict of(Object... keysAndValues) { + final Dict dict = create(); + + String key = null; + for (int i = 0; i < keysAndValues.length; i++) { + if (i % 2 == 0) { + key = Convert.toStr(keysAndValues[i]); + } else { + dict.put(key, keysAndValues[i]); + } + } + + return dict; + } + // --------------------------------------------------------------- Static method end + + // --------------------------------------------------------------- Constructor start + + /** + * 构造 + */ + public Dict() { + this(false); + } + + /** + * 构造 + * + * @param caseInsensitive 是否大小写不敏感 + */ + public Dict(boolean caseInsensitive) { + this(DEFAULT_INITIAL_CAPACITY, caseInsensitive); + } + + /** + * 构造 + * + * @param initialCapacity 初始容量 + */ + public Dict(int initialCapacity) { + this(initialCapacity, false); + } + + /** + * 构造 + * + * @param initialCapacity 初始容量 + * @param caseInsensitive 是否大小写不敏感 + */ + public Dict(int initialCapacity, boolean caseInsensitive) { + this(initialCapacity, DEFAULT_LOAD_FACTOR, caseInsensitive); + } + + /** + * 构造 + * + * @param initialCapacity 初始容量 + * @param loadFactor 容量增长因子,0~1,即达到容量的百分之多少时扩容 + */ + public Dict(int initialCapacity, float loadFactor) { + this(initialCapacity, loadFactor, false); + } + + /** + * 构造 + * + * @param initialCapacity 初始容量 + * @param loadFactor 容量增长因子,0~1,即达到容量的百分之多少时扩容 + * @param caseInsensitive 是否大小写不敏感 + * @since 4.5.16 + */ + public Dict(int initialCapacity, float loadFactor, boolean caseInsensitive) { + super(initialCapacity, loadFactor); + this.caseInsensitive = caseInsensitive; + } + + /** + * 构造 + * + * @param m Map + */ + public Dict(Map m) { + super((null == m) ? new HashMap<>() : m); + } + // --------------------------------------------------------------- Constructor end + + /** + * 转换为Bean对象 + * + * @param Bean类型 + * @param bean Bean + * @return Bean + */ + public T toBean(T bean) { + return toBean(bean, false); + } + + /** + * 转换为Bean对象 + * + * @param Bean类型 + * @param bean Bean + * @return Bean + * @since 3.3.1 + */ + public T toBeanIgnoreCase(T bean) { + BeanUtil.fillBeanWithMapIgnoreCase(this, bean, false); + return bean; + } + + /** + * 转换为Bean对象 + * + * @param Bean类型 + * @param bean Bean + * @param isToCamelCase 是否转换为驼峰模式 + * @return Bean + */ + public T toBean(T bean, boolean isToCamelCase) { + BeanUtil.fillBeanWithMap(this, bean, isToCamelCase, false); + return bean; + } + + /** + * 转换为Bean对象,并使用驼峰法模式转换 + * + * @param Bean类型 + * @param bean Bean + * @return Bean + */ + public T toBeanWithCamelCase(T bean) { + BeanUtil.fillBeanWithMap(this, bean, true, false); + return bean; + } + + /** + * 填充Value Object对象 + * + * @param Bean类型 + * @param clazz Value Object(或者POJO)的类 + * @return vo + */ + public T toBean(Class clazz) { + return BeanUtil.toBean(this, clazz); + } + + /** + * 填充Value Object对象,忽略大小写 + * + * @param Bean类型 + * @param clazz Value Object(或者POJO)的类 + * @return vo + */ + public T toBeanIgnoreCase(Class clazz) { + return BeanUtil.toBeanIgnoreCase(this, clazz, false); + } + + /** + * 将值对象转换为Dict
+ * 类名会被当作表名,小写第一个字母 + * + * @param Bean类型 + * @param bean 值对象 + * @return 自己 + */ + public Dict parseBean(T bean) { + Assert.notNull(bean, "Bean class must be not null"); + this.putAll(BeanUtil.beanToMap(bean)); + return this; + } + + /** + * 将值对象转换为Dict
+ * 类名会被当作表名,小写第一个字母 + * + * @param Bean类型 + * @param bean 值对象 + * @param isToUnderlineCase 是否转换为下划线模式 + * @param ignoreNullValue 是否忽略值为空的字段 + * @return 自己 + */ + public Dict parseBean(T bean, boolean isToUnderlineCase, boolean ignoreNullValue) { + Assert.notNull(bean, "Bean class must be not null"); + this.putAll(BeanUtil.beanToMap(bean, isToUnderlineCase, ignoreNullValue)); + return this; + } + + /** + * 与给定实体对比并去除相同的部分
+ * 此方法用于在更新操作时避免所有字段被更新,跳过不需要更新的字段 version from 2.0.0 + * + * @param 字典对象类型 + * @param dict 字典对象 + * @param withoutNames 不需要去除的字段名 + */ + public void removeEqual(T dict, String... withoutNames) { + HashSet withoutSet = CollUtil.newHashSet(withoutNames); + for (Map.Entry entry : dict.entrySet()) { + if (withoutSet.contains(entry.getKey())) { + continue; + } + + final Object value = this.get(entry.getKey()); + if (Objects.equals(value, entry.getValue())) { + this.remove(entry.getKey()); + } + } + } + + /** + * 过滤Map保留指定键值对,如果键不存在跳过 + * + * @param keys 键列表 + * @return Dict 结果 + * @since 4.0.10 + */ + public Dict filter(String... keys) { + final Dict result = new Dict(keys.length, 1); + + for (String key : keys) { + if (this.containsKey(key)) { + result.put(key, this.get(key)); + } + } + return result; + } + + // -------------------------------------------------------------------- Set start + + /** + * 设置列 + * + * @param attr 属性 + * @param value 值 + * @return 本身 + */ + public Dict set(String attr, Object value) { + this.put(attr, value); + return this; + } + + /** + * 设置列,当键或值为null时忽略 + * + * @param attr 属性 + * @param value 值 + * @return 本身 + */ + public Dict setIgnoreNull(String attr, Object value) { + if (null != attr && null != value) { + set(attr, value); + } + return this; + } + // -------------------------------------------------------------------- Set end + + // -------------------------------------------------------------------- Get start + + @Override + public Object getObj(String key) { + return super.get(key); + } + + /** + * 获得特定类型值 + * + * @param 值类型 + * @param attr 字段名 + * @return 字段值 + * @since 4.6.3 + */ + public T getBean(String attr) { + return get(attr, null); + } + + /** + * 获得特定类型值 + * + * @param 值类型 + * @param attr 字段名 + * @param defaultValue 默认值 + * @return 字段值 + */ + @SuppressWarnings("unchecked") + public T get(String attr, T defaultValue) { + final Object result = get(attr); + return (T) (result != null ? result : defaultValue); + } + + /** + * @param attr 字段名 + * @return 字段值 + */ + @Override + public String getStr(String attr) { + return Convert.toStr(get(attr), null); + } + + /** + * @param attr 字段名 + * @return 字段值 + */ + @Override + public Integer getInt(String attr) { + return Convert.toInt(get(attr), null); + } + + /** + * @param attr 字段名 + * @return 字段值 + */ + @Override + public Long getLong(String attr) { + return Convert.toLong(get(attr), null); + } + + /** + * @param attr 字段名 + * @return 字段值 + */ + @Override + public Float getFloat(String attr) { + return Convert.toFloat(get(attr), null); + } + + @Override + public Short getShort(String attr) { + return Convert.toShort(get(attr), null); + } + + @Override + public Character getChar(String attr) { + return Convert.toChar(get(attr), null); + } + + @Override + public Double getDouble(String attr) { + return Convert.toDouble(get(attr), null); + } + + @Override + public Byte getByte(String attr) { + return Convert.toByte(get(attr), null); + } + + /** + * @param attr 字段名 + * @return 字段值 + */ + @Override + public Boolean getBool(String attr) { + return Convert.toBool(get(attr), null); + } + + /** + * @param attr 字段名 + * @return 字段值 + */ + @Override + public BigDecimal getBigDecimal(String attr) { + return Convert.toBigDecimal(get(attr)); + } + + /** + * @param attr 字段名 + * @return 字段值 + */ + @Override + public BigInteger getBigInteger(String attr) { + return Convert.toBigInteger(get(attr)); + } + + @Override + public > E getEnum(Class clazz, String key) { + return Convert.toEnum(clazz, get(key)); + } + + /** + * @param attr 字段名 + * @return 字段值 + */ + public byte[] getBytes(String attr) { + return get(attr, null); + } + + /** + * @param attr 字段名 + * @return 字段值 + */ + @Override + public Date getDate(String attr) { + return get(attr, null); + } + + + /** + * @param attr 字段名 + * @return 字段值 + */ + public Number getNumber(String attr) { + return get(attr, null); + } + + /** + * 通过表达式获取JSON中嵌套的对象
+ *
    + *
  1. .表达式,可以获取Bean对象中的属性(字段)值或者Map中key对应的值
  2. + *
  3. []表达式,可以获取集合等对象中对应index的值
  4. + *
+ *

+ * 表达式栗子: + * + *

+	 * persion
+	 * persion.name
+	 * persons[3]
+	 * person.friends[5].name
+	 * 
+ * + * @param 目标类型 + * @param expression 表达式 + * @return 对象 + * @see BeanPath#get(Object) + * @since 5.7.14 + */ + @SuppressWarnings("unchecked") + public T getByPath(String expression) { + return (T) BeanPath.create(expression).get(this); + } + + /** + * 通过表达式获取JSON中嵌套的对象
+ *
    + *
  1. .表达式,可以获取Bean对象中的属性(字段)值或者Map中key对应的值
  2. + *
  3. []表达式,可以获取集合等对象中对应index的值
  4. + *
+ *

+ * 表达式栗子: + * + *

+	 * persion
+	 * persion.name
+	 * persons[3]
+	 * person.friends[5].name
+	 * 
+ *

+ * 获取表达式对应值后转换为对应类型的值 + * + * @param 返回值类型 + * @param expression 表达式 + * @param resultType 返回值类型 + * @return 对象 + * @see BeanPath#get(Object) + * @since 5.7.14 + */ + public T getByPath(String expression, Class resultType) { + return Convert.convert(resultType, getByPath(expression)); + } + // -------------------------------------------------------------------- Get end + + @Override + public boolean containsKey(Object key) { + return super.containsKey(customKey((String) key)); + } + + @Override + public Object get(Object key) { + return super.get(customKey((String) key)); + } + + @Override + public Object put(String key, Object value) { + return super.put(customKey(key), value); + } + + @Override + public void putAll(Map m) { + m.forEach(this::put); + } + + @Override + public Dict clone() { + return (Dict) super.clone(); + } + + @Override + public Object remove(Object key) { + return super.remove(customKey((String) key)); + } + + @Override + public boolean remove(Object key, Object value) { + return super.remove(customKey((String) key), value); + } + + @Override + public boolean replace(String key, Object oldValue, Object newValue) { + return super.replace(customKey(key), oldValue, newValue); + } + + @Override + public Object replace(String key, Object value) { + return super.replace(customKey(key), value); + } + + //---------------------------------------------------------------------------- Override default methods start + @Override + public Object getOrDefault(Object key, Object defaultValue) { + return super.getOrDefault(customKey((String) key), defaultValue); + } + + @Override + public Object computeIfPresent(final String key, final BiFunction remappingFunction) { + return super.computeIfPresent(customKey(key), remappingFunction); + } + + @Override + public Object compute(final String key, final BiFunction remappingFunction) { + return super.compute(customKey(key), remappingFunction); + } + + @Override + public Object merge(final String key, final Object value, final BiFunction remappingFunction) { + return super.merge(customKey(key), value, remappingFunction); + } + + @Override + public Object putIfAbsent(String key, Object value) { + return super.putIfAbsent(customKey(key), value); + } + + @Override + public Object computeIfAbsent(String key, Function mappingFunction) { + return super.computeIfAbsent(customKey(key), mappingFunction); + } + + //---------------------------------------------------------------------------- Override default methods end + + /** + * 将Key转为小写 + * + * @param key KEY + * @return 小写KEY + */ + private String customKey(String key) { + if (this.caseInsensitive && null != key) { + key = key.toLowerCase(); + } + return key; + } + + /** + * 通过lambda批量设置值
+ * 实际使用时,可以使用getXXX的方法引用来完成键值对的赋值: + *

+	 *     User user = GenericBuilder.of(User::new).with(User::setUsername, "hutool").build();
+	 *     Dict.create().setFields(user::getNickname, user::getUsername);
+	 * 
+ * + * @param fields lambda,不能为空 + * @return this + * @since 5.7.23 + */ + public Dict setFields(Func0... fields) { + Arrays.stream(fields).forEach(f -> set(LambdaUtil.getFieldName(f), f.callWithRuntimeException())); + return this; + } + +} diff --git a/src/main/java/cn/hutool/core/lang/Editor.java b/src/main/java/cn/hutool/core/lang/Editor.java new file mode 100644 index 0000000..591a873 --- /dev/null +++ b/src/main/java/cn/hutool/core/lang/Editor.java @@ -0,0 +1,24 @@ +package cn.hutool.core.lang; + +/** + * 编辑器接口,常用于对于集合中的元素做统一编辑
+ * 此编辑器两个作用: + * + *
+ * 1、如果返回值为{@code null},表示此值被抛弃
+ * 2、对对象做修改
+ * 
+ * + * @param 被编辑对象类型 + * @author Looly + */ +@FunctionalInterface +public interface Editor { + /** + * 修改过滤后的结果 + * + * @param t 被过滤的对象 + * @return 修改后的对象,如果被过滤返回{@code null} + */ + T edit(T t); +} diff --git a/src/main/java/cn/hutool/core/lang/EnumItem.java b/src/main/java/cn/hutool/core/lang/EnumItem.java new file mode 100644 index 0000000..7a06753 --- /dev/null +++ b/src/main/java/cn/hutool/core/lang/EnumItem.java @@ -0,0 +1,78 @@ +package cn.hutool.core.lang; + +import java.io.Serializable; + +/** + * 枚举元素通用接口,在自定义枚举上实现此接口可以用于数据转换
+ * 数据库保存时建议保存 intVal()而非ordinal()防备需求变更
+ * + * @param Enum类型 + * @author nierjia + * @since 5.4.2 + */ +public interface EnumItem> extends Serializable { + + String name(); + + /** + * 在中文语境下,多数时间枚举会配合一个中文说明 + * + * @return enum名 + */ + default String text() { + return name(); + } + + int intVal(); + + /** + * 获取所有枚举对象 + * + * @return 枚举对象数组 + */ + @SuppressWarnings("unchecked") + default E[] items() { + return (E[]) this.getClass().getEnumConstants(); + } + + /** + * 通过int类型值查找兄弟其他枚举 + * + * @param intVal int值 + * @return Enum + */ + default E fromInt(Integer intVal) { + if (intVal == null) { + return null; + } + E[] vs = items(); + for (E enumItem : vs) { + if (enumItem.intVal() == intVal) { + return enumItem; + } + } + return null; + } + + /** + * 通过String类型的值转换,根据实现可以用name/text + * + * @param strVal String值 + * @return Enum + */ + default E fromStr(String strVal) { + if (strVal == null) { + return null; + } + E[] vs = items(); + for (E enumItem : vs) { + if (strVal.equalsIgnoreCase(enumItem.name())) { + return enumItem; + } + } + return null; + } + + +} + diff --git a/src/main/java/cn/hutool/core/lang/Filter.java b/src/main/java/cn/hutool/core/lang/Filter.java new file mode 100644 index 0000000..f7b98fe --- /dev/null +++ b/src/main/java/cn/hutool/core/lang/Filter.java @@ -0,0 +1,17 @@ +package cn.hutool.core.lang; + +/** + * 过滤器接口 + * + * @author Looly + */ +@FunctionalInterface +public interface Filter { + /** + * 是否接受对象 + * + * @param t 检查的对象 + * @return 是否接受对象 + */ + boolean accept(T t); +} \ No newline at end of file diff --git a/src/main/java/cn/hutool/core/lang/JarClassLoader.java b/src/main/java/cn/hutool/core/lang/JarClassLoader.java new file mode 100644 index 0000000..90bd4bd --- /dev/null +++ b/src/main/java/cn/hutool/core/lang/JarClassLoader.java @@ -0,0 +1,172 @@ +package cn.hutool.core.lang; + +import cn.hutool.core.exceptions.UtilException; +import cn.hutool.core.io.FileUtil; +import cn.hutool.core.util.ClassUtil; +import cn.hutool.core.util.ReflectUtil; +import cn.hutool.core.util.URLUtil; + +import java.io.File; +import java.io.IOException; +import java.lang.reflect.Method; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.List; + +/** + * 外部Jar的类加载器 + * + * @author Looly + */ +public class JarClassLoader extends URLClassLoader { + + /** + * 加载Jar到ClassPath + * + * @param dir jar文件或所在目录 + * @return JarClassLoader + */ + public static JarClassLoader load(File dir) { + final JarClassLoader loader = new JarClassLoader(); + loader.addJar(dir);//查找加载所有jar + loader.addURL(dir);//查找加载所有class + return loader; + } + + /** + * 加载Jar到ClassPath + * + * @param jarFile jar文件或所在目录 + * @return JarClassLoader + */ + public static JarClassLoader loadJar(File jarFile) { + final JarClassLoader loader = new JarClassLoader(); + loader.addJar(jarFile); + return loader; + } + + /** + * 加载Jar文件到指定loader中 + * + * @param loader {@link URLClassLoader} + * @param jarFile 被加载的jar + * @throws UtilException IO异常包装和执行异常 + */ + public static void loadJar(URLClassLoader loader, File jarFile) throws UtilException { + try { + final Method method = ClassUtil.getDeclaredMethod(URLClassLoader.class, "addURL", URL.class); + if (null != method) { + method.setAccessible(true); + final List jars = loopJar(jarFile); + for (File jar : jars) { + ReflectUtil.invoke(loader, method, jar.toURI().toURL()); + } + } + } catch (IOException e) { + throw new UtilException(e); + } + } + + /** + * 加载Jar文件到System ClassLoader中 + * + * @param jarFile 被加载的jar + * @return System ClassLoader + */ + public static URLClassLoader loadJarToSystemClassLoader(File jarFile) { + URLClassLoader urlClassLoader = (URLClassLoader) ClassLoader.getSystemClassLoader(); + loadJar(urlClassLoader, jarFile); + return urlClassLoader; + } + + // ------------------------------------------------------------------- Constructor start + + /** + * 构造 + */ + public JarClassLoader() { + this(new URL[]{}); + } + + /** + * 构造 + * + * @param urls 被加载的URL + */ + public JarClassLoader(URL[] urls) { + super(urls, ClassUtil.getClassLoader()); + } + + /** + * 构造 + * + * @param urls 被加载的URL + * @param classLoader 类加载器 + */ + public JarClassLoader(URL[] urls, ClassLoader classLoader) { + super(urls, classLoader); + } + // ------------------------------------------------------------------- Constructor end + + /** + * 加载Jar文件,或者加载目录 + * + * @param jarFileOrDir jar文件或者jar文件所在目录 + * @return this + */ + public JarClassLoader addJar(File jarFileOrDir) { + if (isJarFile(jarFileOrDir)) { + return addURL(jarFileOrDir); + } + final List jars = loopJar(jarFileOrDir); + for (File jar : jars) { + addURL(jar); + } + return this; + } + + @Override + public void addURL(URL url) { + super.addURL(url); + } + + /** + * 增加class所在目录或文件
+ * 如果为目录,此目录用于搜索class文件,如果为文件,需为jar文件 + * + * @param dir 目录 + * @return this + * @since 4.4.2 + */ + public JarClassLoader addURL(File dir) { + super.addURL(URLUtil.getURL(dir)); + return this; + } + + // ------------------------------------------------------------------- Private method start + + /** + * 递归获得Jar文件 + * + * @param file jar文件或者包含jar文件的目录 + * @return jar文件列表 + */ + private static List loopJar(File file) { + return FileUtil.loopFiles(file, JarClassLoader::isJarFile); + } + + /** + * 是否为jar文件 + * + * @param file 文件 + * @return 是否为jar文件 + * @since 4.4.2 + */ + private static boolean isJarFile(File file) { + if (!FileUtil.isFile(file)) { + return false; + } + return file.getPath().toLowerCase().endsWith(".jar"); + } + // ------------------------------------------------------------------- Private method end +} diff --git a/src/main/java/cn/hutool/core/lang/Matcher.java b/src/main/java/cn/hutool/core/lang/Matcher.java new file mode 100644 index 0000000..5867e7d --- /dev/null +++ b/src/main/java/cn/hutool/core/lang/Matcher.java @@ -0,0 +1,18 @@ +package cn.hutool.core.lang; + +/** + * 匹配接口 + * + * @param 匹配的对象类型 + * @author Looly + */ +@FunctionalInterface +public interface Matcher { + /** + * 给定对象是否匹配 + * + * @param t 对象 + * @return 是否匹配 + */ + boolean match(T t); +} diff --git a/src/main/java/cn/hutool/core/lang/Opt.java b/src/main/java/cn/hutool/core/lang/Opt.java new file mode 100644 index 0000000..86fbe39 --- /dev/null +++ b/src/main/java/cn/hutool/core/lang/Opt.java @@ -0,0 +1,568 @@ +/* + * Copyright (c) 2012, 2020, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package cn.hutool.core.lang; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.lang.func.Func0; +import cn.hutool.core.lang.func.VoidFunc0; +import cn.hutool.core.util.StrUtil; + +import java.util.Collection; +import java.util.NoSuchElementException; +import java.util.Objects; +import java.util.Optional; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.function.Supplier; +import java.util.stream.Stream; + +/** + * 复制jdk16中的Optional,以及自己进行了一点调整和新增,比jdk8中的Optional多了几个实用的函数
+ * 详细见:https://gitee.com/dromara/hutool/pulls/426 + * + * @param 包裹里元素的类型 + * @author VampireAchao + * @see Optional + */ +public class Opt { + /** + * 一个空的{@code Opt} + */ + private static final Opt EMPTY = new Opt<>(null); + + /** + * 返回一个空的{@code Opt} + * + * @param 包裹里元素的类型 + * @return Opt + */ + public static Opt empty() { + @SuppressWarnings("unchecked") final Opt t = (Opt) EMPTY; + return t; + } + + /** + * 返回一个包裹里元素不可能为空的{@code Opt} + * + * @param value 包裹里的元素 + * @param 包裹里元素的类型 + * @return 一个包裹里元素不可能为空的 {@code Opt} + * @throws NullPointerException 如果传入的元素为空,抛出 {@code NPE} + */ + public static Opt of(T value) { + return new Opt<>(Objects.requireNonNull(value)); + } + + /** + * 返回一个包裹里元素可能为空的{@code Opt} + * + * @param value 传入需要包裹的元素 + * @param 包裹里元素的类型 + * @return 一个包裹里元素可能为空的 {@code Opt} + */ + public static Opt ofNullable(T value) { + return value == null ? empty() + : new Opt<>(value); + } + + /** + * 返回一个包裹里元素可能为空的{@code Opt},额外判断了空字符串的情况 + * + * @param value 传入需要包裹的元素 + * @param 包裹里元素的类型 + * @return 一个包裹里元素可能为空,或者为空字符串的 {@code Opt} + */ + public static Opt ofBlankAble(T value) { + return StrUtil.isBlankIfStr(value) ? empty() : new Opt<>(value); + } + + /** + * 返回一个包裹里{@code List}集合可能为空的{@code Opt},额外判断了集合内元素为空的情况 + * + * @param 包裹里元素的类型 + * @param 集合值类型 + * @param value 传入需要包裹的元素 + * @return 一个包裹里元素可能为空的 {@code Opt} + * @since 5.7.17 + */ + public static > Opt ofEmptyAble(R value) { + return CollectionUtil.isEmpty(value) ? empty() : new Opt<>(value); + } + + /** + * @param supplier 操作 + * @param 类型 + * @return 操作执行后的值 + */ + public static Opt ofTry(Func0 supplier) { + try { + return Opt.ofNullable(supplier.call()); + } catch (Exception e) { + final Opt empty = new Opt<>(null); + empty.exception = e; + return empty; + } + } + + /** + * 包裹里实际的元素 + */ + private final T value; + private Exception exception; + + /** + * {@code Opt}的构造函数 + * + * @param value 包裹里的元素 + */ + private Opt(T value) { + this.value = value; + } + + /** + * 返回包裹里的元素,取不到则为{@code null},注意!!!此处和{@link Optional#get()}不同的一点是本方法并不会抛出{@code NoSuchElementException} + * 如果元素为空,则返回{@code null},如果需要一个绝对不能为{@code null}的值,则使用{@link #orElseThrow()} + * + *

+ * 如果需要一个绝对不能为 {@code null}的值,则使用{@link #orElseThrow()} + * 做此处修改的原因是,有时候我们确实需要返回一个null给前端,并且这样的时候并不少见 + * 而使用 {@code .orElse(null)}需要写整整12个字符,用{@code .get()}就只需要6个啦 + * + * @return 包裹里的元素,有可能为{@code null} + */ + public T get() { + return this.value; + } + + /** + * 判断包裹里元素的值是否不存在,不存在为 {@code true},否则为{@code false} + * + * @return 包裹里元素的值不存在 则为 {@code true},否则为{@code false} + * @since 11 这是jdk11{@link Optional}中的新函数 + */ + public boolean isEmpty() { + return value == null; + } + + /** + * 获取异常
+ * 当调用 {@link #ofTry(Func0)}时,异常信息不会抛出,而是保存,调用此方法获取抛出的异常 + * + * @return 异常 + * @since 5.7.17 + */ + public Exception getException() { + return this.exception; + } + + /** + * 是否失败
+ * 当调用 {@link #ofTry(Func0)}时,抛出异常则表示失败 + * + * @return 是否失败 + * @since 5.7.17 + */ + public boolean isFail() { + return null != this.exception; + } + + /** + * 判断包裹里元素的值是否存在,存在为 {@code true},否则为{@code false} + * + * @return 包裹里元素的值存在为 {@code true},否则为{@code false} + */ + public boolean isPresent() { + return value != null; + } + + /** + * 如果包裹里的值存在,就执行传入的操作({@link Consumer#accept}) + * + *

例如如果值存在就打印结果 + *

{@code
+	 * Opt.ofNullable("Hello Hutool!").ifPresent(Console::log);
+	 * }
+ * + * @param action 你想要执行的操作 + * @return this + * @throws NullPointerException 如果包裹里的值存在,但你传入的操作为{@code null}时抛出 + */ + public Opt ifPresent(Consumer action) { + if (isPresent()) { + action.accept(value); + } + return this; + } + + /** + * 如果包裹里的值存在,就执行传入的值存在时的操作({@link Consumer#accept}) + * 否则执行传入的值不存在时的操作({@link VoidFunc0}中的{@link VoidFunc0#call()}) + * + *

+ * 例如值存在就打印对应的值,不存在则用{@code Console.error}打印另一句字符串 + *

{@code
+	 * Opt.ofNullable("Hello Hutool!").ifPresentOrElse(Console::log, () -> Console.error("Ops!Something is wrong!"));
+	 * }
+ * + * @param action 包裹里的值存在时的操作 + * @param emptyAction 包裹里的值不存在时的操作 + * @return this; + * @throws NullPointerException 如果包裹里的值存在时,执行的操作为 {@code null}, 或者包裹里的值不存在时的操作为 {@code null},则抛出{@code NPE} + */ + public Opt ifPresentOrElse(Consumer action, VoidFunc0 emptyAction) { + if (isPresent()) { + action.accept(value); + } else { + emptyAction.callWithRuntimeException(); + } + return this; + } + + + /** + * 如果包裹里的值存在,就执行传入的值存在时的操作({@link Function#apply(Object)})支持链式调用、转换为其他类型 + * 否则执行传入的值不存在时的操作({@link VoidFunc0}中的{@link VoidFunc0#call()}) + * + *

+ * 如果值存在就转换为大写,否则用{@code Console.error}打印另一句字符串 + *

{@code
+	 * String hutool = Opt.ofBlankAble("hutool").mapOrElse(String::toUpperCase, () -> Console.log("yes")).mapOrElse(String::intern, () -> Console.log("Value is not present~")).get();
+	 * }
+ * + * @param map后新的类型 + * @param mapper 包裹里的值存在时的操作 + * @param emptyAction 包裹里的值不存在时的操作 + * @return 新的类型的Opt + * @throws NullPointerException 如果包裹里的值存在时,执行的操作为 {@code null}, 或者包裹里的值不存在时的操作为 {@code null},则抛出{@code NPE} + */ + public Opt mapOrElse(Function mapper, VoidFunc0 emptyAction) { + if (isPresent()) { + return ofNullable(mapper.apply(value)); + } else { + emptyAction.callWithRuntimeException(); + return empty(); + } + } + + /** + * 判断包裹里的值存在并且与给定的条件是否满足 ({@link Predicate#test}执行结果是否为true) + * 如果满足条件则返回本身 + * 不满足条件或者元素本身为空时返回一个返回一个空的{@code Opt} + * + * @param predicate 给定的条件 + * @return 如果满足条件则返回本身, 不满足条件或者元素本身为空时返回一个空的{@code Opt} + * @throws NullPointerException 如果给定的条件为 {@code null},抛出{@code NPE} + */ + public Opt filter(Predicate predicate) { + Objects.requireNonNull(predicate); + if (isEmpty()) { + return this; + } else { + return predicate.test(value) ? this : empty(); + } + } + + /** + * 如果包裹里的值存在,就执行传入的操作({@link Function#apply})并返回一个包裹了该操作返回值的{@code Opt} + * 如果不存在,返回一个空的{@code Opt} + * + * @param mapper 值存在时执行的操作 + * @param 操作返回值的类型 + * @return 如果包裹里的值存在,就执行传入的操作({@link Function#apply})并返回一个包裹了该操作返回值的{@code Opt}, + * 如果不存在,返回一个空的{@code Opt} + * @throws NullPointerException 如果给定的操作为 {@code null},抛出 {@code NPE} + */ + public Opt map(Function mapper) { + Objects.requireNonNull(mapper); + if (isEmpty()) { + return empty(); + } else { + return Opt.ofNullable(mapper.apply(value)); + } + } + + /** + * 如果包裹里的值存在,就执行传入的操作({@link Function#apply})并返回该操作返回值 + * 如果不存在,返回一个空的{@code Opt} + * 和 {@link Opt#map}的区别为 传入的操作返回值必须为 Opt + * + * @param mapper 值存在时执行的操作 + * @param 操作返回值的类型 + * @return 如果包裹里的值存在,就执行传入的操作({@link Function#apply})并返回该操作返回值 + * 如果不存在,返回一个空的{@code Opt} + * @throws NullPointerException 如果给定的操作为 {@code null}或者给定的操作执行结果为 {@code null},抛出 {@code NPE} + */ + public Opt flatMap(Function> mapper) { + Objects.requireNonNull(mapper); + if (isEmpty()) { + return empty(); + } else { + @SuppressWarnings("unchecked") + final Opt r = (Opt) mapper.apply(value); + return Objects.requireNonNull(r); + } + } + + /** + * 如果包裹里的值存在,就执行传入的操作({@link Function#apply})并返回该操作返回值 + * 如果不存在,返回一个空的{@code Opt} + * 和 {@link Opt#map}的区别为 传入的操作返回值必须为 {@link Optional} + * + * @param mapper 值存在时执行的操作 + * @param 操作返回值的类型 + * @return 如果包裹里的值存在,就执行传入的操作({@link Function#apply})并返回该操作返回值 + * 如果不存在,返回一个空的{@code Opt} + * @throws NullPointerException 如果给定的操作为 {@code null}或者给定的操作执行结果为 {@code null},抛出 {@code NPE} + * @see Optional#flatMap(Function) + * @since 5.7.16 + */ + public Opt flattedMap(Function> mapper) { + Objects.requireNonNull(mapper); + if (isEmpty()) { + return empty(); + } else { + return ofNullable(mapper.apply(value).orElse(null)); + } + } + + /** + * 如果包裹里元素的值存在,就执行对应的操作,并返回本身 + * 如果不存在,返回一个空的{@code Opt} + * + *

属于 {@link #ifPresent}的链式拓展 + * + * @param action 值存在时执行的操作 + * @return this + * @throws NullPointerException 如果值存在,并且传入的操作为 {@code null} + * @author VampireAchao + */ + public Opt peek(Consumer action) throws NullPointerException { + Objects.requireNonNull(action); + if (isEmpty()) { + return Opt.empty(); + } + action.accept(value); + return this; + } + + + /** + * 如果包裹里元素的值存在,就执行对应的操作集,并返回本身 + * 如果不存在,返回一个空的{@code Opt} + * + *

属于 {@link #ifPresent}的链式拓展 + *

属于 {@link #peek(Consumer)}的动态拓展 + * + * @param actions 值存在时执行的操作,动态参数,可传入数组,当数组为一个空数组时并不会抛出 {@code NPE} + * @return this + * @throws NullPointerException 如果值存在,并且传入的操作集中的元素为 {@code null} + * @author VampireAchao + */ + @SafeVarargs + public final Opt peeks(Consumer... actions) throws NullPointerException { + // 第三个参数 (opts, opt) -> null其实并不会执行到该函数式接口所以直接返回了个null + return Stream.of(actions).reduce(this, Opt::peek, (opts, opt) -> null); + } + + /** + * 如果包裹里元素的值存在,就返回本身,如果不存在,则使用传入的操作执行后获得的 {@code Opt} + * + * @param supplier 不存在时的操作 + * @return 如果包裹里元素的值存在,就返回本身,如果不存在,则使用传入的函数执行后获得的 {@code Opt} + * @throws NullPointerException 如果传入的操作为空,或者传入的操作执行后返回值为空,则抛出 {@code NPE} + */ + public Opt or(Supplier> supplier) { + Objects.requireNonNull(supplier); + if (isPresent()) { + return this; + } else { + @SuppressWarnings("unchecked") final Opt r = (Opt) supplier.get(); + return Objects.requireNonNull(r); + } + } + + /** + * 如果包裹里元素的值存在,就返回一个包含该元素的 {@link Stream}, + * 否则返回一个空元素的 {@link Stream} + * + *

该方法能将 Opt 中的元素传递给 {@link Stream} + *

{@code
+	 *     Stream> os = ..
+	 *     Stream s = os.flatMap(Opt::stream)
+	 * }
+ * + * @return 返回一个包含该元素的 {@link Stream}或空的 {@link Stream} + */ + public Stream stream() { + if (isEmpty()) { + return Stream.empty(); + } else { + return Stream.of(value); + } + } + + /** + * 如果包裹里元素的值存在,则返回该值,否则返回传入的{@code other} + * + * @param other 元素为空时返回的值,有可能为 {@code null}. + * @return 如果包裹里元素的值存在,则返回该值,否则返回传入的{@code other} + */ + public T orElse(T other) { + return isPresent() ? value : other; + } + + /** + * 异常则返回另一个可选值 + * + * @param other 可选值 + * @return 如果未发生异常,则返回该值,否则返回传入的{@code other} + * @since 5.7.17 + */ + public T exceptionOrElse(T other) { + return isFail() ? other : value; + } + + /** + * 如果包裹里元素的值存在,则返回该值,否则返回传入的操作执行后的返回值 + * + * @param supplier 值不存在时需要执行的操作,返回一个类型与 包裹里元素类型 相同的元素 + * @return 如果包裹里元素的值存在,则返回该值,否则返回传入的操作执行后的返回值 + * @throws NullPointerException 如果之不存在,并且传入的操作为空,则抛出 {@code NPE} + */ + public T orElseGet(Supplier supplier) { + return isPresent() ? value : supplier.get(); + } + + /** + * 如果包裹里的值存在,则返回该值,否则抛出 {@code NoSuchElementException} + * + * @return 返回一个不为 {@code null} 的包裹里的值 + * @throws NoSuchElementException 如果包裹里的值不存在则抛出该异常 + */ + public T orElseThrow() { + return orElseThrow(NoSuchElementException::new, "No value present"); + } + + /** + * 如果包裹里的值存在,则返回该值,否则执行传入的操作,获取异常类型的返回值并抛出 + *

往往是一个包含无参构造器的异常 例如传入{@code IllegalStateException::new} + * + * @param 异常类型 + * @param exceptionSupplier 值不存在时执行的操作,返回值继承 {@link Throwable} + * @return 包裹里不能为空的值 + * @throws X 如果值不存在 + * @throws NullPointerException 如果值不存在并且 传入的操作为 {@code null}或者操作执行后的返回值为{@code null} + */ + public T orElseThrow(Supplier exceptionSupplier) throws X { + if (isPresent()) { + return value; + } else { + throw exceptionSupplier.get(); + } + } + + /** + * 如果包裹里的值存在,则返回该值,否则执行传入的操作,获取异常类型的返回值并抛出 + * + *

往往是一个包含 自定义消息 构造器的异常 例如 + *

{@code
+	 * 		Opt.ofNullable(null).orElseThrow(IllegalStateException::new, "Ops!Something is wrong!");
+	 * }
+ * + * @param 异常类型 + * @param exceptionFunction 值不存在时执行的操作,返回值继承 {@link Throwable} + * @param message 作为传入操作执行时的参数,一般作为异常自定义提示语 + * @return 包裹里不能为空的值 + * @throws X 如果值不存在 + * @throws NullPointerException 如果值不存在并且 传入的操作为 {@code null}或者操作执行后的返回值为{@code null} + * @author VampireAchao + */ + public T orElseThrow(Function exceptionFunction, String message) throws X { + if (isPresent()) { + return value; + } else { + throw exceptionFunction.apply(message); + } + } + + /** + * 转换为 {@link Optional}对象 + * + * @return {@link Optional}对象 + * @since 5.7.16 + */ + public Optional toOptional() { + return Optional.ofNullable(this.value); + } + + /** + * 判断传入参数是否与 {@code Opt}相等 + * 在以下情况下返回true + *
    + *
  • 它也是一个 {@code Opt} 并且 + *
  • 它们包裹住的元素都为空 或者 + *
  • 它们包裹住的元素之间相互 {@code equals()} + *
+ * + * @param obj 一个要用来判断是否相等的参数 + * @return 如果传入的参数也是一个 {@code Opt}并且它们包裹住的元素都为空 + * 或者它们包裹住的元素之间相互 {@code equals()} 就返回{@code true} + * 否则返回 {@code false} + */ + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + + if (!(obj instanceof Opt)) { + return false; + } + + final Opt other = (Opt) obj; + return Objects.equals(value, other.value); + } + + /** + * 如果包裹内元素为空,则返回0,否则返回元素的 {@code hashcode} + * + * @return 如果包裹内元素为空,则返回0,否则返回元素的 {@code hashcode} + */ + @Override + public int hashCode() { + return Objects.hashCode(value); + } + + /** + * 返回包裹内元素调用{@code toString()}的结果,不存在则返回{@code null} + * + * @return 包裹内元素调用{@code toString()}的结果,不存在则返回{@code null} + */ + @Override + public String toString() { + return StrUtil.toStringOrNull(this.value); + } +} diff --git a/src/main/java/cn/hutool/core/lang/Pair.java b/src/main/java/cn/hutool/core/lang/Pair.java new file mode 100644 index 0000000..a7cb9cd --- /dev/null +++ b/src/main/java/cn/hutool/core/lang/Pair.java @@ -0,0 +1,87 @@ +package cn.hutool.core.lang; + +import cn.hutool.core.clone.CloneSupport; + +import java.io.Serializable; +import java.util.Objects; + +/** + * 键值对对象,只能在构造时传入键值 + * + * @param 键类型 + * @param 值类型 + * @author looly + * @since 4.1.5 + */ +public class Pair extends CloneSupport> implements Serializable { + private static final long serialVersionUID = 1L; + + protected K key; + protected V value; + + /** + * 构建{@code Pair}对象 + * + * @param 键类型 + * @param 值类型 + * @param key 键 + * @param value 值 + * @return {@code Pair} + * @since 5.4.3 + */ + public static Pair of(K key, V value) { + return new Pair<>(key, value); + } + + /** + * 构造 + * + * @param key 键 + * @param value 值 + */ + public Pair(K key, V value) { + this.key = key; + this.value = value; + } + + /** + * 获取键 + * + * @return 键 + */ + public K getKey() { + return this.key; + } + + /** + * 获取值 + * + * @return 值 + */ + public V getValue() { + return this.value; + } + + @Override + public String toString() { + return "Pair [key=" + key + ", value=" + value + "]"; + } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o instanceof Pair) { + Pair pair = (Pair) o; + return Objects.equals(getKey(), pair.getKey()) && + Objects.equals(getValue(), pair.getValue()); + } + return false; + } + + @Override + public int hashCode() { + //copy from 1.8 HashMap.Node + return Objects.hashCode(key) ^ Objects.hashCode(value); + } +} diff --git a/src/main/java/cn/hutool/core/lang/ParameterizedTypeImpl.java b/src/main/java/cn/hutool/core/lang/ParameterizedTypeImpl.java new file mode 100644 index 0000000..7077238 --- /dev/null +++ b/src/main/java/cn/hutool/core/lang/ParameterizedTypeImpl.java @@ -0,0 +1,102 @@ +package cn.hutool.core.lang; + +import java.io.Serializable; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; + +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.StrUtil; + +/** + * {@link ParameterizedType} 接口实现,用于重新定义泛型类型 + * + * @author looly + * @since 4.5.7 + */ +public class ParameterizedTypeImpl implements ParameterizedType, Serializable { + private static final long serialVersionUID = 1L; + + private final Type[] actualTypeArguments; + private final Type ownerType; + private final Type rawType; + + /** + * 构造 + * + * @param actualTypeArguments 实际的泛型参数类型 + * @param ownerType 拥有者类型 + * @param rawType 原始类型 + */ + public ParameterizedTypeImpl(Type[] actualTypeArguments, Type ownerType, Type rawType) { + this.actualTypeArguments = actualTypeArguments; + this.ownerType = ownerType; + this.rawType = rawType; + } + + @Override + public Type[] getActualTypeArguments() { + return actualTypeArguments; + } + + @Override + public Type getOwnerType() { + return ownerType; + } + + @Override + public Type getRawType() { + return rawType; + } + + @Override + public String toString() { + final StringBuilder buf = new StringBuilder(); + + final Type useOwner = this.ownerType; + final Class raw = (Class) this.rawType; + if (useOwner == null) { + buf.append(raw.getName()); + } else { + if (useOwner instanceof Class) { + buf.append(((Class) useOwner).getName()); + } else { + buf.append(useOwner); + } + buf.append('.').append(raw.getSimpleName()); + } + + appendAllTo(buf.append('<'), ", ", this.actualTypeArguments).append('>'); + return buf.toString(); + } + + /** + * 追加 {@code types} 到 @{code buf},使用 {@code sep} 分隔 + * + * @param buf 目标 + * @param sep 分隔符 + * @param types 加入的类型 + * @return {@code buf} + */ + private static StringBuilder appendAllTo(final StringBuilder buf, final String sep, final Type... types) { + if (ArrayUtil.isNotEmpty(types)) { + boolean isFirst = true; + for (Type type : types) { + if (isFirst) { + isFirst = false; + } else { + buf.append(sep); + } + + String typeStr; + if(type instanceof Class) { + typeStr = ((Class)type).getName(); + }else { + typeStr = StrUtil.toString(type); + } + + buf.append(typeStr); + } + } + return buf; + } +} diff --git a/src/main/java/cn/hutool/core/lang/PatternPool.java b/src/main/java/cn/hutool/core/lang/PatternPool.java new file mode 100644 index 0000000..20aa6cf --- /dev/null +++ b/src/main/java/cn/hutool/core/lang/PatternPool.java @@ -0,0 +1,275 @@ +package cn.hutool.core.lang; + +import cn.hutool.core.map.WeakConcurrentMap; + +import java.util.regex.Pattern; + +/** + * 常用正则表达式集合,更多正则见:
+ * https://any86.github.io/any-rule/ + * + * @author Looly + */ +public class PatternPool { + + /** + * 英文字母 、数字和下划线 + */ + public final static Pattern GENERAL = Pattern.compile(RegexPool.GENERAL); + /** + * 数字 + */ + public final static Pattern NUMBERS = Pattern.compile(RegexPool.NUMBERS); + /** + * 字母 + */ + public final static Pattern WORD = Pattern.compile(RegexPool.WORD); + /** + * 单个中文汉字 + */ + public final static Pattern CHINESE = Pattern.compile(RegexPool.CHINESE); + /** + * 中文汉字 + */ + public final static Pattern CHINESES = Pattern.compile(RegexPool.CHINESES); + /** + * 分组 + */ + public final static Pattern GROUP_VAR = Pattern.compile(RegexPool.GROUP_VAR); + /** + * IP v4 + */ + public final static Pattern IPV4 = Pattern.compile(RegexPool.IPV4); + /** + * IP v6 + */ + public final static Pattern IPV6 = Pattern.compile(RegexPool.IPV6); + /** + * 货币 + */ + public final static Pattern MONEY = Pattern.compile(RegexPool.MONEY); + /** + * 邮件,符合RFC 5322规范,正则来自:http://emailregex.com/
+ * https://stackoverflow.com/questions/386294/what-is-the-maximum-length-of-a-valid-email-address/44317754 + * 注意email 要宽松一点。比如 jetz.chong@hutool.cn、jetz-chong@ hutool.cn、jetz_chong@hutool.cn、dazhi.duan@hutool.cn 宽松一点把,都算是正常的邮箱 + */ + public final static Pattern EMAIL = Pattern.compile(RegexPool.EMAIL, Pattern.CASE_INSENSITIVE); + /** + * 移动电话 + */ + public final static Pattern MOBILE = Pattern.compile(RegexPool.MOBILE); + /** + * 中国香港移动电话 + * eg: 中国香港: +852 5100 4810, 三位区域码+10位数字, 中国香港手机号码8位数 + * eg: 中国大陆: +86 180 4953 1399,2位区域码标示+13位数字 + * 中国大陆 +86 Mainland China + * 中国香港 +852 Hong Kong + * 中国澳门 +853 Macao + * 中国台湾 +886 Taiwan + */ + public final static Pattern MOBILE_HK = Pattern.compile(RegexPool.MOBILE_HK); + /** + * 中国台湾移动电话 + * eg: 中国台湾: +886 09 60 000000, 三位区域码+号码以数字09开头 + 8位数字, 中国台湾手机号码10位数 + * 中国台湾 +886 Taiwan 国际域名缩写:TW + */ + public final static Pattern MOBILE_TW = Pattern.compile(RegexPool.MOBILE_TW); + /** + * 中国澳门移动电话 + * eg: 中国台湾: +853 68 00000, 三位区域码 +号码以数字6开头 + 7位数字, 中国台湾手机号码8位数 + * 中国澳门 +853 Macao 国际域名缩写:MO + */ + public final static Pattern MOBILE_MO = Pattern.compile(RegexPool.MOBILE_MO); + /** + * 座机号码 + */ + public final static Pattern TEL = Pattern.compile(RegexPool.TEL); + /** + * 座机号码+400+800电话 + * + * @see 800 + */ + public final static Pattern TEL_400_800 = Pattern.compile(RegexPool.TEL_400_800); + /** + * 18位身份证号码 + */ + public final static Pattern CITIZEN_ID = Pattern.compile(RegexPool.CITIZEN_ID); + /** + * 邮编,兼容港澳台 + */ + public final static Pattern ZIP_CODE = Pattern.compile(RegexPool.ZIP_CODE); + /** + * 生日 + */ + public final static Pattern BIRTHDAY = Pattern.compile(RegexPool.BIRTHDAY); + /** + * URL + */ + public final static Pattern URL = Pattern.compile(RegexPool.URL); + /** + * Http URL + */ + public final static Pattern URL_HTTP = Pattern.compile(RegexPool.URL_HTTP, Pattern.CASE_INSENSITIVE); + /** + * 中文字、英文字母、数字和下划线 + */ + public final static Pattern GENERAL_WITH_CHINESE = Pattern.compile(RegexPool.GENERAL_WITH_CHINESE); + /** + * UUID + */ + public final static Pattern UUID = Pattern.compile(RegexPool.UUID, Pattern.CASE_INSENSITIVE); + /** + * 不带横线的UUID + */ + public final static Pattern UUID_SIMPLE = Pattern.compile(RegexPool.UUID_SIMPLE); + /** + * MAC地址正则 + */ + public static final Pattern MAC_ADDRESS = Pattern.compile(RegexPool.MAC_ADDRESS, Pattern.CASE_INSENSITIVE); + /** + * 16进制字符串 + */ + public static final Pattern HEX = Pattern.compile(RegexPool.HEX); + /** + * 时间正则 + */ + public static final Pattern TIME = Pattern.compile(RegexPool.TIME); + /** + * 中国车牌号码(兼容新能源车牌) + */ + public final static Pattern PLATE_NUMBER = Pattern.compile(RegexPool.PLATE_NUMBER); + + /** + * 统一社会信用代码 + *
+	 * 第一部分:登记管理部门代码1位 (数字或大写英文字母)
+	 * 第二部分:机构类别代码1位 (数字或大写英文字母)
+	 * 第三部分:登记管理机关行政区划码6位 (数字)
+	 * 第四部分:主体标识码(组织机构代码)9位 (数字或大写英文字母)
+	 * 第五部分:校验码1位 (数字或大写英文字母)
+	 * 
+ */ + public static final Pattern CREDIT_CODE = Pattern.compile(RegexPool.CREDIT_CODE); + /** + * 车架号 + * 别名:车辆识别代号 车辆识别码 + * eg:LDC613P23A1305189 + * eg:LSJA24U62JG269225 + * 十七位码、车架号 + * 车辆的唯一标示 + */ + public static final Pattern CAR_VIN = Pattern.compile(RegexPool.CAR_VIN); + /** + * 驾驶证 别名:驾驶证档案编号、行驶证编号 + * eg:430101758218 + * 12位数字字符串 + * 仅限:中国驾驶证档案编号 + */ + public static final Pattern CAR_DRIVING_LICENCE = Pattern.compile(RegexPool.CAR_DRIVING_LICENCE); + /** + * 中文姓名 + * 总结中国人姓名:2-60位,只能是中文和 · + */ + public static final Pattern CHINESE_NAME = Pattern.compile(RegexPool.CHINESE_NAME); + + // ------------------------------------------------------------------------------------------------------------------------------------------------------------------- + /** + * Pattern池 + */ + private static final WeakConcurrentMap POOL = new WeakConcurrentMap<>(); + + /** + * 先从Pattern池中查找正则对应的{@link Pattern},找不到则编译正则表达式并入池。 + * + * @param regex 正则表达式 + * @return {@link Pattern} + */ + public static Pattern get(String regex) { + return get(regex, 0); + } + + /** + * 先从Pattern池中查找正则对应的{@link Pattern},找不到则编译正则表达式并入池。 + * + * @param regex 正则表达式 + * @param flags 正则标识位集合 {@link Pattern} + * @return {@link Pattern} + */ + public static Pattern get(String regex, int flags) { + final RegexWithFlag regexWithFlag = new RegexWithFlag(regex, flags); + return POOL.computeIfAbsent(regexWithFlag, (key)-> Pattern.compile(regex, flags)); + } + + /** + * 移除缓存 + * + * @param regex 正则 + * @param flags 标识 + * @return 移除的{@link Pattern},可能为{@code null} + */ + public static Pattern remove(String regex, int flags) { + return POOL.remove(new RegexWithFlag(regex, flags)); + } + + /** + * 清空缓存池 + */ + public static void clear() { + POOL.clear(); + } + + // --------------------------------------------------------------------------------------------------------------------------------- + + /** + * 正则表达式和正则标识位的包装 + * + * @author Looly + */ + private static class RegexWithFlag { + private final String regex; + private final int flag; + + /** + * 构造 + * + * @param regex 正则 + * @param flag 标识 + */ + public RegexWithFlag(String regex, int flag) { + this.regex = regex; + this.flag = flag; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + flag; + result = prime * result + ((regex == null) ? 0 : regex.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + RegexWithFlag other = (RegexWithFlag) obj; + if (flag != other.flag) { + return false; + } + if (regex == null) { + return other.regex == null; + } else { + return regex.equals(other.regex); + } + } + + } +} diff --git a/src/main/java/cn/hutool/core/lang/Range.java b/src/main/java/cn/hutool/core/lang/Range.java new file mode 100644 index 0000000..de4b0db --- /dev/null +++ b/src/main/java/cn/hutool/core/lang/Range.java @@ -0,0 +1,233 @@ +package cn.hutool.core.lang; + +import cn.hutool.core.thread.lock.NoLock; + +import java.io.Serializable; +import java.util.Iterator; +import java.util.NoSuchElementException; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; + +/** + * 范围生成器。根据给定的初始值、结束值和步进生成一个步进列表生成器
+ * 由于用户自行实现{@link Stepper}来定义步进,因此Range本身无法判定边界(是否达到end),需在step实现边界判定逻辑。 + * + *

+ * 此类使用{@link ReentrantReadWriteLock}保证线程安全 + *

+ * + * @param 生成范围对象的类型 + * @author Looly + */ +public class Range implements Iterable, Iterator, Serializable { + private static final long serialVersionUID = 1L; + + /** + * 锁保证线程安全 + */ + private Lock lock = new ReentrantLock(); + /** + * 起始对象 + */ + private final T start; + /** + * 结束对象 + */ + private final T end; + /** + * 下一个对象 + */ + private T next; + /** + * 步进 + */ + private final Stepper stepper; + /** + * 索引 + */ + private int index = 0; + /** + * 是否包含第一个元素 + */ + private final boolean includeStart; + /** + * 是否包含最后一个元素 + */ + private final boolean includeEnd; + + /** + * 构造 + * + * @param start 起始对象(包括) + * @param stepper 步进 + */ + public Range(T start, Stepper stepper) { + this(start, null, stepper); + } + + /** + * 构造 + * + * @param start 起始对象(包含) + * @param end 结束对象(包含) + * @param stepper 步进 + */ + public Range(T start, T end, Stepper stepper) { + this(start, end, stepper, true, true); + } + + /** + * 构造 + * + * @param start 起始对象 + * @param end 结束对象 + * @param stepper 步进 + * @param isIncludeStart 是否包含第一个元素 + * @param isIncludeEnd 是否包含最后一个元素 + */ + public Range(T start, T end, Stepper stepper, boolean isIncludeStart, boolean isIncludeEnd) { + Assert.notNull(start, "First element must be not null!"); + this.start = start; + this.end = end; + this.stepper = stepper; + this.next = safeStep(this.start); + this.includeStart = isIncludeStart; + this.includeEnd = isIncludeEnd; + } + + /** + * 禁用锁,调用此方法后不再使用锁保护 + * + * @return this + * @since 4.3.1 + */ + public Range disableLock() { + this.lock = new NoLock(); + return this; + } + + @Override + public boolean hasNext() { + lock.lock(); + try { + if (0 == this.index && this.includeStart) { + return true; + } + if (null == this.next) { + return false; + } else if (!includeEnd && this.next.equals(this.end)) { + return false; + } + } finally { + lock.unlock(); + } + return true; + } + + @Override + public T next() { + lock.lock(); + try { + if (!this.hasNext()) { + throw new NoSuchElementException("Has no next range!"); + } + return nextUncheck(); + } finally { + lock.unlock(); + } + } + + /** + * 获取下一个元素,并将下下个元素准备好 + */ + private T nextUncheck() { + T current; + if(0 == this.index){ + current = start; + if(!this.includeStart){ + // 获取下一组元素 + index ++; + return nextUncheck(); + } + } else { + current = next; + this.next = safeStep(this.next); + } + + index++; + return current; + } + + /** + * 不抛异常的获取下一步进的元素,如果获取失败返回{@code null} + * + * @param base 上一个元素 + * @return 下一步进 + */ + private T safeStep(T base) { + final int index = this.index; + T next = null; + try { + next = stepper.step(base, this.end, index); + } catch (Exception e) { + // ignore + } + + return next; + } + + @Override + public void remove() { + throw new UnsupportedOperationException("Can not remove ranged element!"); + } + + @Override + public Iterator iterator() { + return this; + } + + /** + * 重置Range + * + * @return this + */ + public Range reset() { + lock.lock(); + try { + this.index = 0; + this.next = safeStep(this.start); + } finally { + lock.unlock(); + } + return this; + } + + /** + * 步进接口,此接口用于实现如何对一个对象按照指定步进增加步进
+ * 步进接口可以定义以下逻辑: + * + *
+	 * 1、步进规则,即对象如何做步进
+	 * 2、步进大小,通过实现此接口,在实现类中定义一个对象属性,可灵活定义步进大小
+	 * 3、限制range个数,通过实现此接口,在实现类中定义一个对象属性,可灵活定义limit,限制range个数
+	 * 
+ * + * @param 需要增加步进的对象 + * @author Looly + */ + @FunctionalInterface + public interface Stepper { + /** + * 增加步进
+ * 增加步进后的返回值如果为{@code null}则表示步进结束
+ * 用户需根据end参数自行定义边界,当达到边界时返回null表示结束,否则Range中边界对象无效,会导致无限循环 + * + * @param current 上一次增加步进后的基础对象 + * @param end 结束对象 + * @param index 当前索引(步进到第几个元素),从0开始计数 + * @return 增加步进后的对象 + */ + T step(T current, T end, int index); + } +} diff --git a/src/main/java/cn/hutool/core/lang/RegexPool.java b/src/main/java/cn/hutool/core/lang/RegexPool.java new file mode 100644 index 0000000..ed8a73e --- /dev/null +++ b/src/main/java/cn/hutool/core/lang/RegexPool.java @@ -0,0 +1,203 @@ +package cn.hutool.core.lang; + +/** + * 常用正则表达式字符串池 + * + * @author looly + * @since 5.7.3 + */ +public interface RegexPool { + /** + * 英文字母 、数字和下划线 + */ + String GENERAL = "^\\w+$"; + /** + * 数字 + */ + String NUMBERS = "\\d+"; + /** + * 字母 + */ + String WORD = "[a-zA-Z]+"; + /** + * 单个中文汉字
+ * 参照维基百科汉字Unicode范围(https://zh.wikipedia.org/wiki/%E6%B1%89%E5%AD%97 页面右侧) + */ + String CHINESE = "[\u2E80-\u2EFF\u2F00-\u2FDF\u31C0-\u31EF\u3400-\u4DBF\u4E00-\u9FFF\uF900-\uFAFF\uD840\uDC00-\uD869\uDEDF\uD869\uDF00-\uD86D\uDF3F\uD86D\uDF40-\uD86E\uDC1F\uD86E\uDC20-\uD873\uDEAF\uD87E\uDC00-\uD87E\uDE1F]"; + /** + * 中文汉字 + */ + String CHINESES = CHINESE + "+"; + /** + * 分组 + */ + String GROUP_VAR = "\\$(\\d+)"; + /** + * IP v4
+ * 采用分组方式便于解析地址的每一个段 + */ + //String IPV4 = "\\b((?!\\d\\d\\d)\\d+|1\\d\\d|2[0-4]\\d|25[0-5])\\.((?!\\d\\d\\d)\\d+|1\\d\\d|2[0-4]\\d|25[0-5])\\.((?!\\d\\d\\d)\\d+|1\\d\\d|2[0-4]\\d|25[0-5])\\.((?!\\d\\d\\d)\\d+|1\\d\\d|2[0-4]\\d|25[0-5])\\b"; + String IPV4 = "^(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)\\.(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)\\.(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)\\.(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)$"; + /** + * IP v6 + */ + String IPV6 = "(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]+|::(ffff(:0{1,4})?:)?((25[0-5]|(2[0-4]|1?[0-9])?[0-9])\\.){3}(25[0-5]|(2[0-4]|1?[0-9])?[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1?[0-9])?[0-9])\\.){3}(25[0-5]|(2[0-4]|1?[0-9])?[0-9]))"; + /** + * 货币 + */ + String MONEY = "^(\\d+(?:\\.\\d+)?)$"; + /** + * 邮件,符合RFC 5322规范,正则来自:http://emailregex.com/ + * What is the maximum length of a valid email address? https://stackoverflow.com/questions/386294/what-is-the-maximum-length-of-a-valid-email-address/44317754 + * 注意email 要宽松一点。比如 jetz.chong@hutool.cn、jetz-chong@ hutool.cn、jetz_chong@hutool.cn、dazhi.duan@hutool.cn 宽松一点把,都算是正常的邮箱 + */ + String EMAIL = "(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|\"(?:[\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x21\\x23-\\x5b\\x5d-\\x7f]|\\\\[\\x01-\\x09\\x0b\\x0c\\x0e-\\x7f])*\")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\\[(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|[a-z0-9-]*[a-z0-9]:(?:[\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x21-\\x5a\\x53-\\x7f]|\\\\[\\x01-\\x09\\x0b\\x0c\\x0e-\\x7f])+)])"; + /** + * 移动电话 + * eg: 中国大陆: +86 180 4953 1399,2位区域码标示+11位数字 + * 中国大陆 +86 Mainland China + */ + String MOBILE = "(?:0|86|\\+86)?1[3-9]\\d{9}"; + /** + * 中国香港移动电话 + * eg: 中国香港: +852 5100 4810, 三位区域码+10位数字, 中国香港手机号码8位数 + */ + String MOBILE_HK = "(?:0|852|\\+852)?\\d{8}"; + /** + * 中国台湾移动电话 + * eg: 中国台湾: +886 09 60 000000, 三位区域码+号码以数字09开头 + 8位数字, 中国台湾手机号码10位数 + * 中国台湾 +886 Taiwan 国际域名缩写:TW + */ + String MOBILE_TW = "(?:0|886|\\+886)?(?:|-)09\\d{8}"; + /** + * 中国澳门移动电话 + * eg: 中国澳门: +853 68 00000, 三位区域码 +号码以数字6开头 + 7位数字, 中国澳门手机号码8位数 + * 中国澳门 +853 Macao 国际域名缩写:MO + */ + String MOBILE_MO = "(?:0|853|\\+853)?(?:|-)6\\d{7}"; + /** + * 座机号码
+ * pr#387@Gitee + */ + String TEL = "(010|02\\d|0[3-9]\\d{2})-?(\\d{6,8})"; + /** + * 座机号码+400+800电话 + * + * @see 800 + */ + String TEL_400_800 = "0\\d{2,3}[\\- ]?[1-9]\\d{6,7}|[48]00[\\- ]?[1-9]\\d{2}[\\- ]?\\d{4}"; + /** + * 18位身份证号码 + */ + String CITIZEN_ID = "[1-9]\\d{5}[1-2]\\d{3}((0\\d)|(1[0-2]))(([012]\\d)|3[0-1])\\d{3}(\\d|X|x)"; + /** + * 邮编,兼容港澳台 + */ + String ZIP_CODE = "^(0[1-7]|1[0-356]|2[0-7]|3[0-6]|4[0-7]|5[0-7]|6[0-7]|7[0-5]|8[0-9]|9[0-8])\\d{4}|99907[78]$"; + /** + * 生日 + */ + String BIRTHDAY = "^(\\d{2,4})([/\\-.年]?)(\\d{1,2})([/\\-.月]?)(\\d{1,2})日?$"; + /** + * URI
+ * 定义见:https://www.ietf.org/rfc/rfc3986.html#appendix-B + */ + String URI = "^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\\?([^#]*))?(#(.*))?"; + /** + * URL + */ + String URL = "[a-zA-Z]+://[\\w-+&@#/%?=~_|!:,.;]*[\\w-+&@#/%=~_|]"; + /** + * Http URL(来自:http://urlregex.com/)
+ * 此正则同时支持FTP、File等协议的URL + */ + String URL_HTTP = "(https?|ftp|file)://[\\w-+&@#/%?=~_|!:,.;]*[\\w-+&@#/%=~_|]"; + /** + * 中文字、英文字母、数字和下划线 + */ + String GENERAL_WITH_CHINESE = "^[\u4E00-\u9FFF\\w]+$"; + /** + * UUID + */ + String UUID = "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"; + /** + * 不带横线的UUID + */ + String UUID_SIMPLE = "^[0-9a-fA-F]{32}$"; + /** + * MAC地址正则 + */ + String MAC_ADDRESS = "((?:[a-fA-F0-9]{1,2}[:-]){5}[a-fA-F0-9]{1,2})|0x(\\d{12}).+ETHER"; + /** + * 16进制字符串 + */ + String HEX = "^[a-fA-F0-9]+$"; + /** + * 时间正则 + */ + String TIME = "\\d{1,2}:\\d{1,2}(:\\d{1,2})?"; + /** + * 中国车牌号码(兼容新能源车牌) + */ + String PLATE_NUMBER = + //https://gitee.com/dromara/hutool/issues/I1B77H?from=project-issue + "^(([京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领][A-Z](([0-9]{5}[ABCDEFGHJK])|([ABCDEFGHJK]([A-HJ-NP-Z0-9])[0-9]{4})))|" + + //https://gitee.com/dromara/hutool/issues/I1BJHE?from=project-issue + "([京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领]\\d{3}\\d{1,3}[领])|" + + "([京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领][A-Z][A-HJ-NP-Z0-9]{4}[A-HJ-NP-Z0-9挂学警港澳使领]))$"; + + /** + * 统一社会信用代码 + *
+	 * 第一部分:登记管理部门代码1位 (数字或大写英文字母)
+	 * 第二部分:机构类别代码1位 (数字或大写英文字母)
+	 * 第三部分:登记管理机关行政区划码6位 (数字)
+	 * 第四部分:主体标识码(组织机构代码)9位 (数字或大写英文字母)
+	 * 第五部分:校验码1位 (数字或大写英文字母)
+	 * 
+ */ + String CREDIT_CODE = "^[0-9A-HJ-NPQRTUWXY]{2}\\d{6}[0-9A-HJ-NPQRTUWXY]{10}$"; + /** + * 车架号 + * 别名:车辆识别代号 车辆识别码 + * eg:LDC613P23A1305189 + * eg:LSJA24U62JG269225 + * 十七位码、车架号 + * 车辆的唯一标示 + */ + String CAR_VIN = "^[A-HJ-NPR-Z0-9]{8}[0-9X][A-HJ-NPR-Z0-9]{2}\\d{6}$"; + /** + * 驾驶证 别名:驾驶证档案编号、行驶证编号 + * eg:430101758218 + * 12位数字字符串 + * 仅限:中国驾驶证档案编号 + */ + String CAR_DRIVING_LICENCE = "^[0-9]{12}$"; + /** + * 中文姓名 + * 维吾尔族姓名里面的点是 · 输入法中文状态下,键盘左上角数字1前面的那个符号;
+ * 错误字符:{@code ..。..}
+ * 正确维吾尔族姓名: + *
+	 * 霍加阿卜杜拉·麦提喀斯木
+	 * 玛合萨提别克·哈斯木别克
+	 * 阿布都热依木江·艾斯卡尔
+	 * 阿卜杜尼亚孜·毛力尼亚孜
+	 * 
+ *
+	 * ----------
+	 * 错误示例:孟  伟                reason: 有空格
+	 * 错误示例:连逍遥0               reason: 数字
+	 * 错误示例:依帕古丽-艾则孜        reason: 特殊符号
+	 * 错误示例:牙力空.买提萨力        reason: 新疆人的点不对
+	 * 错误示例:王建鹏2002-3-2        reason: 有数字、特殊符号
+	 * 错误示例:雷金默(雷皓添)        reason: 有括号
+	 * 错误示例:翟冬:亮               reason: 有特殊符号
+	 * 错误示例:李                   reason: 少于2位
+	 * ----------
+	 * 
+ * 总结中文姓名:2-60位,只能是中文和维吾尔族的点· + * 放宽汉字范围:如生僻姓名 刘欣䶮yǎn + */ + String CHINESE_NAME = "^[\u2E80-\u9FFF·]{2,60}$"; +} diff --git a/src/main/java/cn/hutool/core/lang/Replacer.java b/src/main/java/cn/hutool/core/lang/Replacer.java new file mode 100644 index 0000000..03af67f --- /dev/null +++ b/src/main/java/cn/hutool/core/lang/Replacer.java @@ -0,0 +1,22 @@ +package cn.hutool.core.lang; + +/** + * 替换器
+ * 通过实现此接口完成指定类型对象的替换操作,替换后的目标类型依旧为指定类型 + * + * @author looly + * + * @param 被替换操作的类型 + * @since 4.1.5 + */ +@FunctionalInterface +public interface Replacer { + + /** + * 替换指定类型为目标类型 + * + * @param t 被替换的对象 + * @return 替代后的对象 + */ + T replace(T t); +} diff --git a/src/main/java/cn/hutool/core/lang/ResourceClassLoader.java b/src/main/java/cn/hutool/core/lang/ResourceClassLoader.java new file mode 100644 index 0000000..5f83efc --- /dev/null +++ b/src/main/java/cn/hutool/core/lang/ResourceClassLoader.java @@ -0,0 +1,73 @@ +package cn.hutool.core.lang; + +import cn.hutool.core.io.resource.Resource; +import cn.hutool.core.util.ClassLoaderUtil; +import cn.hutool.core.util.ObjectUtil; + +import java.security.SecureClassLoader; +import java.util.HashMap; +import java.util.Map; + +/** + * 资源类加载器,可以加载任意类型的资源类 + * + * @param {@link Resource}接口实现类 + * @author looly, lzpeng + * @since 5.5.2 + */ +public class ResourceClassLoader extends SecureClassLoader { + + private final Map resourceMap; + /** + * 缓存已经加载的类 + */ + private final Map> cacheClassMap; + + /** + * 构造 + * + * @param parentClassLoader 父类加载器,null表示默认当前上下文加载器 + * @param resourceMap 资源map + */ + public ResourceClassLoader(ClassLoader parentClassLoader, Map resourceMap) { + super(ObjectUtil.defaultIfNull(parentClassLoader, ClassLoaderUtil::getClassLoader)); + this.resourceMap = ObjectUtil.defaultIfNull(resourceMap, new HashMap<>()); + this.cacheClassMap = new HashMap<>(); + } + + /** + * 增加需要加载的类资源 + * + * @param resource 资源,可以是文件、流或者字符串 + * @return this + */ + public ResourceClassLoader addResource(T resource) { + this.resourceMap.put(resource.getName(), resource); + return this; + } + + @Override + protected Class findClass(String name) throws ClassNotFoundException { + final Class clazz = cacheClassMap.computeIfAbsent(name, this::defineByName); + if (clazz == null) { + return super.findClass(name); + } + return clazz; + } + + /** + * 从给定资源中读取class的二进制流,然后生成类
+ * 如果这个类资源不存在,返回{@code null} + * + * @param name 类名 + * @return 定义的类 + */ + private Class defineByName(String name) { + final Resource resource = resourceMap.get(name); + if (null != resource) { + final byte[] bytes = resource.readBytes(); + return defineClass(name, bytes, 0, bytes.length); + } + return null; + } +} diff --git a/src/main/java/cn/hutool/core/lang/Segment.java b/src/main/java/cn/hutool/core/lang/Segment.java new file mode 100644 index 0000000..1b511cf --- /dev/null +++ b/src/main/java/cn/hutool/core/lang/Segment.java @@ -0,0 +1,41 @@ +package cn.hutool.core.lang; + +import cn.hutool.core.convert.Convert; +import cn.hutool.core.util.NumberUtil; + +import java.lang.reflect.Type; + +/** + * 片段表示,用于表示文本、集合等数据结构的一个区间。 + * @param 数字类型,用于表示位置index + * + * @author looly + * @since 5.5.3 + */ +public interface Segment { + + /** + * 获取起始位置 + * + * @return 起始位置 + */ + T getStartIndex(); + + /** + * 获取结束位置 + * + * @return 结束位置 + */ + T getEndIndex(); + + /** + * 片段长度,默认计算方法为abs({@link #getEndIndex()} - {@link #getEndIndex()}) + * + * @return 片段长度 + */ + default T length(){ + final T start = Assert.notNull(getStartIndex(), "Start index must be not null!"); + final T end = Assert.notNull(getEndIndex(), "End index must be not null!"); + return Convert.convert((Type) start.getClass(), NumberUtil.sub(end, start).abs()); + } +} diff --git a/src/main/java/cn/hutool/core/lang/SimpleCache.java b/src/main/java/cn/hutool/core/lang/SimpleCache.java new file mode 100644 index 0000000..a8ada35 --- /dev/null +++ b/src/main/java/cn/hutool/core/lang/SimpleCache.java @@ -0,0 +1,192 @@ +package cn.hutool.core.lang; + +import cn.hutool.core.collection.TransIter; +import cn.hutool.core.lang.func.Func0; +import cn.hutool.core.lang.mutable.Mutable; +import cn.hutool.core.lang.mutable.MutableObj; +import cn.hutool.core.map.SafeConcurrentHashMap; +import cn.hutool.core.map.WeakConcurrentMap; + +import java.io.Serializable; +import java.util.Iterator; +import java.util.Map; +import java.util.WeakHashMap; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; +import java.util.function.Predicate; + +/** + * 简单缓存,无超时实现,默认使用{@link WeakConcurrentMap}实现缓存自动清理 + * + * @param 键类型 + * @param 值类型 + * @author Looly + */ +public class SimpleCache implements Iterable>, Serializable { + private static final long serialVersionUID = 1L; + + /** + * 池 + */ + private final Map, V> rawMap; + // 乐观读写锁 + private final ReadWriteLock lock = new ReentrantReadWriteLock(); + /** + * 写的时候每个key一把锁,降低锁的粒度 + */ + protected final Map keyLockMap = new SafeConcurrentHashMap<>(); + + /** + * 构造,默认使用{@link WeakHashMap}实现缓存自动清理 + */ + public SimpleCache() { + this(new WeakConcurrentMap<>()); + } + + /** + * 构造 + *

+ * 通过自定义Map初始化,可以自定义缓存实现。
+ * 比如使用{@link WeakHashMap}则会自动清理key,使用HashMap则不会清理
+ * 同时,传入的Map对象也可以自带初始化的键值对,防止在get时创建 + *

+ * + * @param initMap 初始Map,用于定义Map类型 + */ + public SimpleCache(Map, V> initMap) { + this.rawMap = initMap; + } + + /** + * 从缓存池中查找值 + * + * @param key 键 + * @return 值 + */ + public V get(K key) { + lock.readLock().lock(); + try { + return rawMap.get(MutableObj.of(key)); + } finally { + lock.readLock().unlock(); + } + } + + /** + * 从缓存中获得对象,当对象不在缓存中或已经过期返回Func0回调产生的对象 + * + * @param key 键 + * @param supplier 如果不存在回调方法,用于生产值对象 + * @return 值对象 + */ + public V get(K key, Func0 supplier) { + return get(key, null, supplier); + } + + /** + * 从缓存中获得对象,当对象不在缓存中或已经过期返回Func0回调产生的对象 + * + * @param key 键 + * @param validPredicate 检查结果对象是否可用,如是否断开连接等 + * @param supplier 如果不存在回调方法或结果不可用,用于生产值对象 + * @return 值对象 + * @since 5.7.9 + */ + public V get(K key, Predicate validPredicate, Func0 supplier) { + V v = get(key); + if((null != validPredicate && null != v && !validPredicate.test(v))){ + v = null; + } + if (null == v && null != supplier) { + //每个key单独获取一把锁,降低锁的粒度提高并发能力,see pr#1385@Github + final Lock keyLock = keyLockMap.computeIfAbsent(key, k -> new ReentrantLock()); + keyLock.lock(); + try { + // 双重检查,防止在竞争锁的过程中已经有其它线程写入 + v = get(key); + if (null == v || (null != validPredicate && !validPredicate.test(v))) { + try { + v = supplier.call(); + } catch (Exception e) { + throw new RuntimeException(e); + } + put(key, v); + } + } finally { + keyLock.unlock(); + keyLockMap.remove(key); + } + } + + return v; + } + + /** + * 放入缓存 + * + * @param key 键 + * @param value 值 + * @return 值 + */ + public V put(K key, V value) { + // 独占写锁 + lock.writeLock().lock(); + try { + rawMap.put(MutableObj.of(key), value); + } finally { + lock.writeLock().unlock(); + } + return value; + } + + /** + * 移除缓存 + * + * @param key 键 + * @return 移除的值 + */ + public V remove(K key) { + // 独占写锁 + lock.writeLock().lock(); + try { + return rawMap.remove(MutableObj.of(key)); + } finally { + lock.writeLock().unlock(); + } + } + + /** + * 清空缓存池 + */ + public void clear() { + // 独占写锁 + lock.writeLock().lock(); + try { + this.rawMap.clear(); + } finally { + lock.writeLock().unlock(); + } + } + + @Override + public Iterator> iterator() { + return new TransIter<>(this.rawMap.entrySet().iterator(), (entry)-> new Map.Entry() { + @Override + public K getKey() { + return entry.getKey().get(); + } + + @Override + public V getValue() { + return entry.getValue(); + } + + @Override + public V setValue(V value) { + return entry.setValue(value); + } + }); + } +} diff --git a/src/main/java/cn/hutool/core/lang/Singleton.java b/src/main/java/cn/hutool/core/lang/Singleton.java new file mode 100644 index 0000000..97a7fa5 --- /dev/null +++ b/src/main/java/cn/hutool/core/lang/Singleton.java @@ -0,0 +1,161 @@ +package cn.hutool.core.lang; + +import cn.hutool.core.lang.func.Func0; +import cn.hutool.core.map.SafeConcurrentHashMap; +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.ClassUtil; +import cn.hutool.core.util.ReflectUtil; +import cn.hutool.core.util.StrUtil; + +import java.util.Set; +import java.util.stream.Collectors; + +/** + * 单例类
+ * 提供单例对象的统一管理,当调用get方法时,如果对象池中存在此对象,返回此对象,否则创建新对象返回
+ * + * @author loolly + */ +public final class Singleton { + + private static final SafeConcurrentHashMap POOL = new SafeConcurrentHashMap<>(); + + private Singleton() { + } + + /** + * 获得指定类的单例对象
+ * 对象存在于池中返回,否则创建,每次调用此方法获得的对象为同一个对象
+ * 注意:单例针对的是类和参数,也就是说只有类、参数一致才会返回同一个对象 + * + * @param 单例对象类型 + * @param clazz 类 + * @param params 构造方法参数 + * @return 单例对象 + */ + public static T get(Class clazz, Object... params) { + Assert.notNull(clazz, "Class must be not null !"); + final String key = buildKey(clazz.getName(), params); + return get(key, () -> ReflectUtil.newInstance(clazz, params)); + } + + /** + * 获得指定类的单例对象
+ * 对象存在于池中返回,否则创建,每次调用此方法获得的对象为同一个对象
+ * + * @param 单例对象类型 + * @param key 自定义键 + * @param supplier 单例对象的创建函数 + * @return 单例对象 + * @since 5.3.3 + */ + @SuppressWarnings("unchecked") + public static T get(String key, Func0 supplier) { + return (T) POOL.computeIfAbsent(key, (k)-> supplier.callWithRuntimeException()); + } + + /** + * 获得指定类的单例对象
+ * 对象存在于池中返回,否则创建,每次调用此方法获得的对象为同一个对象
+ * + * @param 单例对象类型 + * @param className 类名 + * @param params 构造参数 + * @return 单例对象 + */ + public static T get(String className, Object... params) { + Assert.notBlank(className, "Class name must be not blank !"); + final Class clazz = ClassUtil.loadClass(className); + return get(clazz, params); + } + + /** + * 将已有对象放入单例中,其Class做为键 + * + * @param obj 对象 + * @since 4.0.7 + */ + public static void put(Object obj) { + Assert.notNull(obj, "Bean object must be not null !"); + put(obj.getClass().getName(), obj); + } + + /** + * 将已有对象放入单例中,key做为键 + * + * @param key 键 + * @param obj 对象 + * @since 5.3.3 + */ + public static void put(String key, Object obj) { + POOL.put(key, obj); + } + + /** + * 判断某个类的对象是否存在 + * + * @param clazz 类 + * @param params 构造参数 + * @return 是否存在 + */ + public static boolean exists(Class clazz, Object... params){ + if (null != clazz){ + final String key = buildKey(clazz.getName(), params); + return POOL.containsKey(key); + } + return false; + } + + /** + * 获取单例池中存在的所有类 + * + * @return 非重复的类集合 + */ + public static Set> getExistClass(){ + return POOL.values().stream().map(Object::getClass).collect(Collectors.toSet()); + } + + /** + * 移除指定Singleton对象 + * + * @param clazz 类 + */ + public static void remove(Class clazz) { + if (null != clazz) { + remove(clazz.getName()); + } + } + + /** + * 移除指定Singleton对象 + * + * @param key 键 + */ + public static void remove(String key) { + POOL.remove(key); + } + + /** + * 清除所有Singleton对象 + */ + public static void destroy() { + POOL.clear(); + } + + // ------------------------------------------------------------------------------------------- Private method start + + /** + * 构建key + * + * @param className 类名 + * @param params 参数列表 + * @return key + */ + private static String buildKey(String className, Object... params) { + if (ArrayUtil.isEmpty(params)) { + return className; + } + return StrUtil.format("{}#{}", className, ArrayUtil.join(params, "_")); + } + // ------------------------------------------------------------------------------------------- Private method end +} diff --git a/src/main/java/cn/hutool/core/lang/Tuple.java b/src/main/java/cn/hutool/core/lang/Tuple.java new file mode 100644 index 0000000..6dbd9f2 --- /dev/null +++ b/src/main/java/cn/hutool/core/lang/Tuple.java @@ -0,0 +1,179 @@ +package cn.hutool.core.lang; + +import cn.hutool.core.clone.CloneSupport; +import cn.hutool.core.collection.ArrayIter; +import cn.hutool.core.collection.ListUtil; +import cn.hutool.core.util.ArrayUtil; + +import java.io.Serializable; +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; +import java.util.Spliterator; +import java.util.Spliterators; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; + +/** + * 不可变数组类型(元组),用于多值返回
+ * 多值可以支持每个元素值类型不同 + * + * @author Looly + */ +public class Tuple extends CloneSupport implements Iterable, Serializable { + private static final long serialVersionUID = -7689304393482182157L; + + private final Object[] members; + private int hashCode; + private boolean cacheHash; + + /** + * 构造 + * + * @param members 成员数组 + */ + public Tuple(Object... members) { + this.members = members; + } + + /** + * 获取指定位置元素 + * + * @param 返回对象类型 + * @param index 位置 + * @return 元素 + */ + @SuppressWarnings("unchecked") + public T get(int index) { + return (T) members[index]; + } + + /** + * 获得所有元素 + * + * @return 获得所有元素 + */ + public Object[] getMembers() { + return this.members; + } + + /** + * 将元组转换成列表 + * + * @return 转换得到的列表 + * @since 5.6.6 + */ + public final List toList() { + return ListUtil.toList(this.members); + } + + /** + * 缓存Hash值,当为true时,此对象的hash值只被计算一次,常用于Tuple中的值不变时使用。 + * 注意:当为true时,member变更对象后,hash值不会变更。 + * + * @param cacheHash 是否缓存hash值 + * @return this + * @since 5.2.1 + */ + public Tuple setCacheHash(boolean cacheHash) { + this.cacheHash = cacheHash; + return this; + } + + /** + * 得到元组的大小 + * + * @return 元组的大小 + * @since 5.6.6 + */ + public int size() { + return this.members.length; + } + + /** + * 判断元组中是否包含某元素 + * + * @param value 需要判定的元素 + * @return 是否包含 + * @since 5.6.6 + */ + public boolean contains(Object value) { + return ArrayUtil.contains(this.members, value); + } + + /** + * 将元组转成流 + * + * @return 流 + * @since 5.6.6 + */ + public final Stream stream() { + return Arrays.stream(this.members); + } + + /** + * 将元组转成并行流 + * + * @return 流 + * @since 5.6.6 + */ + public final Stream parallelStream() { + return StreamSupport.stream(spliterator(), true); + } + + /** + * 截取元组指定部分 + * + * @param start 起始位置(包括) + * @param end 终止位置(不包括) + * @return 截取得到的元组 + * @since 5.6.6 + */ + public final Tuple sub(final int start, final int end) { + return new Tuple(ArrayUtil.sub(this.members, start, end)); + } + + @Override + public int hashCode() { + if (this.cacheHash && 0 != this.hashCode) { + return this.hashCode; + } + final int prime = 31; + int result = 1; + result = prime * result + Arrays.deepHashCode(members); + if (this.cacheHash) { + this.hashCode = result; + } + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + Tuple other = (Tuple) obj; + return Arrays.deepEquals(members, other.members); + } + + @Override + public String toString() { + return Arrays.toString(members); + } + + @Override + public Iterator iterator() { + return new ArrayIter<>(members); + } + + @Override + public final Spliterator spliterator() { + return Spliterators.spliterator(this.members, Spliterator.ORDERED); + } +} diff --git a/src/main/java/cn/hutool/core/lang/TypeReference.java b/src/main/java/cn/hutool/core/lang/TypeReference.java new file mode 100644 index 0000000..ccb9f18 --- /dev/null +++ b/src/main/java/cn/hutool/core/lang/TypeReference.java @@ -0,0 +1,51 @@ +package cn.hutool.core.lang; + +import java.lang.reflect.Type; + +import cn.hutool.core.util.TypeUtil; + +/** + * Type类型参考
+ * 通过构建一个类型参考子类,可以获取其泛型参数中的Type类型。例如: + * + *
+ * TypeReference<List<String>> list = new TypeReference<List<String>>() {};
+ * Type t = tr.getType();
+ * 
+ * + * 此类无法应用于通配符泛型参数(wildcard parameters),比如:{@code Class} 或者 {@code List? extends CharSequence>} + * + *

+ * 此类参考FastJSON的TypeReference实现 + * + * @author looly + * + * @param 需要自定义的参考类型 + * @since 4.2.2 + */ +public abstract class TypeReference implements Type { + + /** 泛型参数 */ + private final Type type; + + /** + * 构造 + */ + public TypeReference() { + this.type = TypeUtil.getTypeArgument(getClass()); + } + + /** + * 获取用户定义的泛型参数 + * + * @return 泛型参数 + */ + public Type getType() { + return this.type; + } + + @Override + public String toString() { + return this.type.toString(); + } +} diff --git a/src/main/java/cn/hutool/core/lang/UUID.java b/src/main/java/cn/hutool/core/lang/UUID.java new file mode 100644 index 0000000..e0c7fba --- /dev/null +++ b/src/main/java/cn/hutool/core/lang/UUID.java @@ -0,0 +1,447 @@ +package cn.hutool.core.lang; + +import cn.hutool.core.util.RandomUtil; +import cn.hutool.core.util.StrUtil; + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.util.Random; + +/** + * 提供通用唯一识别码(universally unique identifier)(UUID)实现,UUID表示一个128位的值。
+ * 此类拷贝自java.util.UUID,用于生成不带-的UUID字符串 + * + *

+ * 这些通用标识符具有不同的变体。此类的方法用于操作 Leach-Salz 变体,不过构造方法允许创建任何 UUID 变体(将在下面进行描述)。 + *

+ * 变体 2 (Leach-Salz) UUID 的布局如下: long 型数据的最高有效位由以下无符号字段组成: + * + *

+ * 0xFFFFFFFF00000000 time_low
+ * 0x00000000FFFF0000 time_mid
+ * 0x000000000000F000 version
+ * 0x0000000000000FFF time_hi
+ * 
+ *

+ * long 型数据的最低有效位由以下无符号字段组成: + * + *

+ * 0xC000000000000000 variant
+ * 0x3FFF000000000000 clock_seq
+ * 0x0000FFFFFFFFFFFF node
+ * 
+ * + *

+ * variant 字段包含一个表示 UUID 布局的值。以上描述的位布局仅在 UUID 的 variant 值为 2(表示 Leach-Salz 变体)时才有效。 * + *

+ * version 字段保存描述此 UUID 类型的值。有 4 种不同的基本 UUID 类型:基于时间的 UUID、DCE 安全 UUID、基于名称的 UUID 和随机生成的 UUID。
+ * 这些类型的 version 值分别为 1、2、3 和 4。 + * + * @since 4.1.11 + */ +public class UUID implements java.io.Serializable, Comparable { + private static final long serialVersionUID = -1185015143654744140L; + + /** + * {@link SecureRandom} 的单例 + * + * @author looly + */ + private static class Holder { + static final SecureRandom NUMBER_GENERATOR = RandomUtil.getSecureRandom(); + } + + /** + * 此UUID的最高64有效位 + */ + private final long mostSigBits; + + /** + * 此UUID的最低64有效位 + */ + private final long leastSigBits; + + /** + * 私有构造 + * + * @param data 数据 + */ + private UUID(byte[] data) { + long msb = 0; + long lsb = 0; + assert data.length == 16 : "data must be 16 bytes in length"; + for (int i = 0; i < 8; i++) { + msb = (msb << 8) | (data[i] & 0xff); + } + for (int i = 8; i < 16; i++) { + lsb = (lsb << 8) | (data[i] & 0xff); + } + this.mostSigBits = msb; + this.leastSigBits = lsb; + } + + /** + * 使用指定的数据构造新的 UUID。 + * + * @param mostSigBits 用于 {@code UUID} 的最高有效 64 位 + * @param leastSigBits 用于 {@code UUID} 的最低有效 64 位 + */ + public UUID(long mostSigBits, long leastSigBits) { + this.mostSigBits = mostSigBits; + this.leastSigBits = leastSigBits; + } + + /** + * 获取类型 4(伪随机生成的)UUID 的静态工厂。 使用加密的本地线程伪随机数生成器生成该 UUID。 + * + * @return 随机生成的 {@code UUID} + */ + public static UUID fastUUID() { + return randomUUID(false); + } + + /** + * 获取类型 4(伪随机生成的)UUID 的静态工厂。 使用加密的强伪随机数生成器生成该 UUID。 + * + * @return 随机生成的 {@code UUID} + */ + public static UUID randomUUID() { + return randomUUID(true); + } + + /** + * 获取类型 4(伪随机生成的)UUID 的静态工厂。 使用加密的强伪随机数生成器生成该 UUID。 + * + * @param isSecure 是否使用{@link SecureRandom}如果是可以获得更安全的随机码,否则可以得到更好的性能 + * @return 随机生成的 {@code UUID} + */ + public static UUID randomUUID(boolean isSecure) { + final Random ng = isSecure ? Holder.NUMBER_GENERATOR : RandomUtil.getRandom(); + + final byte[] randomBytes = new byte[16]; + ng.nextBytes(randomBytes); + + randomBytes[6] &= 0x0f; /* clear version */ + randomBytes[6] |= 0x40; /* set to version 4 */ + randomBytes[8] &= 0x3f; /* clear variant */ + randomBytes[8] |= 0x80; /* set to IETF variant */ + + return new UUID(randomBytes); + } + + /** + * 根据指定的字节数组获取类型 3(基于名称的)UUID 的静态工厂。 + * + * @param name 用于构造 UUID 的字节数组。 + * @return 根据指定数组生成的 {@code UUID} + */ + public static UUID nameUUIDFromBytes(byte[] name) { + MessageDigest md; + try { + md = MessageDigest.getInstance("MD5"); + } catch (NoSuchAlgorithmException nsae) { + throw new InternalError("MD5 not supported"); + } + byte[] md5Bytes = md.digest(name); + md5Bytes[6] &= 0x0f; /* clear version */ + md5Bytes[6] |= 0x30; /* set to version 3 */ + md5Bytes[8] &= 0x3f; /* clear variant */ + md5Bytes[8] |= 0x80; /* set to IETF variant */ + return new UUID(md5Bytes); + } + + /** + * 根据 {@link #toString()} 方法中描述的字符串标准表示形式创建{@code UUID}。 + * + * @param name 指定 {@code UUID} 字符串 + * @return 具有指定值的 {@code UUID} + * @throws IllegalArgumentException 如果 name 与 {@link #toString} 中描述的字符串表示形式不符抛出此异常 + */ + public static UUID fromString(String name) { + String[] components = name.split("-"); + if (components.length != 5) { + throw new IllegalArgumentException("Invalid UUID string: " + name); + } + for (int i = 0; i < 5; i++) { + components[i] = "0x" + components[i]; + } + + long mostSigBits = Long.decode(components[0]); + mostSigBits <<= 16; + mostSigBits |= Long.decode(components[1]); + mostSigBits <<= 16; + mostSigBits |= Long.decode(components[2]); + + long leastSigBits = Long.decode(components[3]); + leastSigBits <<= 48; + leastSigBits |= Long.decode(components[4]); + + return new UUID(mostSigBits, leastSigBits); + } + + /** + * 返回此 UUID 的 128 位值中的最低有效 64 位。 + * + * @return 此 UUID 的 128 位值中的最低有效 64 位。 + */ + public long getLeastSignificantBits() { + return leastSigBits; + } + + /** + * 返回此 UUID 的 128 位值中的最高有效 64 位。 + * + * @return 此 UUID 的 128 位值中最高有效 64 位。 + */ + public long getMostSignificantBits() { + return mostSigBits; + } + + /** + * 与此 {@code UUID} 相关联的版本号. 版本号描述此 {@code UUID} 是如何生成的。 + *

+ * 版本号具有以下含意: + *

    + *
  • 1 基于时间的 UUID + *
  • 2 DCE 安全 UUID + *
  • 3 基于名称的 UUID + *
  • 4 随机生成的 UUID + *
+ * + * @return 此 {@code UUID} 的版本号 + */ + public int version() { + // Version is bits masked by 0x000000000000F000 in MS long + return (int) ((mostSigBits >> 12) & 0x0f); + } + + /** + * 与此 {@code UUID} 相关联的变体号。变体号描述 {@code UUID} 的布局。 + *

+ * 变体号具有以下含意: + *

    + *
  • 0 为 NCS 向后兼容保留 + *
  • 2 IETF RFC 4122(Leach-Salz), 用于此类 + *
  • 6 保留,微软向后兼容 + *
  • 7 保留供以后定义使用 + *
+ * + * @return 此 {@code UUID} 相关联的变体号 + */ + public int variant() { + // This field is composed of a varying number of bits. + // 0 - - Reserved for NCS backward compatibility + // 1 0 - The IETF aka Leach-Salz variant (used by this class) + // 1 1 0 Reserved, Microsoft backward compatibility + // 1 1 1 Reserved for future definition. + return (int) ((leastSigBits >>> (64 - (leastSigBits >>> 62))) & (leastSigBits >> 63)); + } + + /** + * 与此 UUID 相关联的时间戳值。 + * + *

+ * 60 位的时间戳值根据此 {@code UUID} 的 time_low、time_mid 和 time_hi 字段构造。
+ * 所得到的时间戳以 100 毫微秒为单位,从 UTC(通用协调时间) 1582 年 10 月 15 日零时开始。 + * + *

+ * 时间戳值仅在在基于时间的 UUID(其 version 类型为 1)中才有意义。
+ * 如果此 {@code UUID} 不是基于时间的 UUID,则此方法抛出 UnsupportedOperationException。 + * + * @return 时间戳值 + * @throws UnsupportedOperationException 如果此 {@code UUID} 不是 version 为 1 的 UUID。 + */ + public long timestamp() throws UnsupportedOperationException { + checkTimeBase(); + return (mostSigBits & 0x0FFFL) << 48// + | ((mostSigBits >> 16) & 0x0FFFFL) << 32// + | mostSigBits >>> 32; + } + + /** + * 与此 UUID 相关联的时钟序列值。 + * + *

+ * 14 位的时钟序列值根据此 UUID 的 clock_seq 字段构造。clock_seq 字段用于保证在基于时间的 UUID 中的时间唯一性。 + *

+ * {@code clockSequence} 值仅在基于时间的 UUID(其 version 类型为 1)中才有意义。 如果此 UUID 不是基于时间的 UUID,则此方法抛出 UnsupportedOperationException。 + * + * @return 此 {@code UUID} 的时钟序列 + * @throws UnsupportedOperationException 如果此 UUID 的 version 不为 1 + */ + public int clockSequence() throws UnsupportedOperationException { + checkTimeBase(); + return (int) ((leastSigBits & 0x3FFF000000000000L) >>> 48); + } + + /** + * 与此 UUID 相关的节点值。 + * + *

+ * 48 位的节点值根据此 UUID 的 node 字段构造。此字段旨在用于保存机器的 IEEE 802 地址,该地址用于生成此 UUID 以保证空间唯一性。 + *

+ * 节点值仅在基于时间的 UUID(其 version 类型为 1)中才有意义。
+ * 如果此 UUID 不是基于时间的 UUID,则此方法抛出 UnsupportedOperationException。 + * + * @return 此 {@code UUID} 的节点值 + * @throws UnsupportedOperationException 如果此 UUID 的 version 不为 1 + */ + public long node() throws UnsupportedOperationException { + checkTimeBase(); + return leastSigBits & 0x0000FFFFFFFFFFFFL; + } + + // Object Inherited Methods + + /** + * 返回此{@code UUID} 的字符串表现形式。 + * + *

+ * UUID 的字符串表示形式由此 BNF 描述: + * + *

+	 * {@code
+	 * UUID                   = ----
+	 * time_low               = 4*
+	 * time_mid               = 2*
+	 * time_high_and_version  = 2*
+	 * variant_and_sequence   = 2*
+	 * node                   = 6*
+	 * hexOctet               = 
+	 * hexDigit               = [0-9a-fA-F]
+	 * }
+	 * 
+ * + * @return 此{@code UUID} 的字符串表现形式 + * @see #toString(boolean) + */ + @Override + public String toString() { + return toString(false); + } + + /** + * 返回此{@code UUID} 的字符串表现形式。 + * + *

+ * UUID 的字符串表示形式由此 BNF 描述: + * + *

+	 * {@code
+	 * UUID                   = ----
+	 * time_low               = 4*
+	 * time_mid               = 2*
+	 * time_high_and_version  = 2*
+	 * variant_and_sequence   = 2*
+	 * node                   = 6*
+	 * hexOctet               = 
+	 * hexDigit               = [0-9a-fA-F]
+	 * }
+	 * 
+ * + * @param isSimple 是否简单模式,简单模式为不带'-'的UUID字符串 + * @return 此{@code UUID} 的字符串表现形式 + */ + public String toString(boolean isSimple) { + final StringBuilder builder = StrUtil.builder(isSimple ? 32 : 36); + // time_low + builder.append(digits(mostSigBits >> 32, 8)); + if (!isSimple) { + builder.append('-'); + } + // time_mid + builder.append(digits(mostSigBits >> 16, 4)); + if (!isSimple) { + builder.append('-'); + } + // time_high_and_version + builder.append(digits(mostSigBits, 4)); + if (!isSimple) { + builder.append('-'); + } + // variant_and_sequence + builder.append(digits(leastSigBits >> 48, 4)); + if (!isSimple) { + builder.append('-'); + } + // node + builder.append(digits(leastSigBits, 12)); + + return builder.toString(); + } + + /** + * 返回此 UUID 的哈希码。 + * + * @return UUID 的哈希码值。 + */ + @Override + public int hashCode() { + long hilo = mostSigBits ^ leastSigBits; + return ((int) (hilo >> 32)) ^ (int) hilo; + } + + /** + * 将此对象与指定对象比较。 + *

+ * 当且仅当参数不为 {@code null}、而是一个 UUID 对象、具有与此 UUID 相同的 varriant、包含相同的值(每一位均相同)时,结果才为 {@code true}。 + * + * @param obj 要与之比较的对象 + * @return 如果对象相同,则返回 {@code true};否则返回 {@code false} + */ + @Override + public boolean equals(Object obj) { + if ((null == obj) || (obj.getClass() != UUID.class)) { + return false; + } + UUID id = (UUID) obj; + return (mostSigBits == id.mostSigBits && leastSigBits == id.leastSigBits); + } + + // Comparison Operations + + /** + * 将此 UUID 与指定的 UUID 比较。 + * + *

+ * 如果两个 UUID 不同,且第一个 UUID 的最高有效字段大于第二个 UUID 的对应字段,则第一个 UUID 大于第二个 UUID。 + * + * @param val 与此 UUID 比较的 UUID + * @return 在此 UUID 小于、等于或大于 val 时,分别返回 -1、0 或 1。 + */ + @Override + public int compareTo(UUID val) { + // The ordering is intentionally set up so that the UUIDs + // can simply be numerically compared as two numbers + int compare = Long.compare(this.mostSigBits, val.mostSigBits); + if(0 == compare){ + compare = Long.compare(this.leastSigBits, val.leastSigBits); + } + return compare; + } + + // ------------------------------------------------------------------------------------------------------------------- Private method start + + /** + * 返回指定数字对应的hex值 + * + * @param val 值 + * @param digits 位 + * @return 值 + */ + private static String digits(long val, int digits) { + long hi = 1L << (digits * 4); + return Long.toHexString(hi | (val & (hi - 1))).substring(1); + } + + /** + * 检查是否为time-based版本UUID + */ + private void checkTimeBase() { + if (version() != 1) { + throw new UnsupportedOperationException("Not a time-based UUID"); + } + } + // ------------------------------------------------------------------------------------------------------------------- Private method end +} diff --git a/src/main/java/cn/hutool/core/lang/Validator.java b/src/main/java/cn/hutool/core/lang/Validator.java new file mode 100644 index 0000000..e2eb396 --- /dev/null +++ b/src/main/java/cn/hutool/core/lang/Validator.java @@ -0,0 +1,1239 @@ +package cn.hutool.core.lang; + +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.exceptions.ValidateException; +import cn.hutool.core.util.CreditCodeUtil; +import cn.hutool.core.util.NumberUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.ReUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.core.util.IdcardUtil; + +import java.net.MalformedURLException; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * 字段验证器(验证器),分两种类型的验证: + * + *

    + *
  • isXXX:通过返回boolean值判断是否满足给定格式。
  • + *
  • validateXXX:通过抛出异常{@link ValidateException}检查是否满足给定格式。
  • + *
+ *

+ * 主要验证字段非空、是否为满足指定格式等(如是否为Email、电话等) + * + * @author Looly + */ +public class Validator { + + /** + * 英文字母 、数字和下划线 + */ + public final static Pattern GENERAL = PatternPool.GENERAL; + /** + * 数字 + */ + public final static Pattern NUMBERS = PatternPool.NUMBERS; + /** + * 分组 + */ + public final static Pattern GROUP_VAR = PatternPool.GROUP_VAR; + /** + * IP v4 + */ + public final static Pattern IPV4 = PatternPool.IPV4; + /** + * IP v6 + */ + public final static Pattern IPV6 = PatternPool.IPV6; + /** + * 货币 + */ + public final static Pattern MONEY = PatternPool.MONEY; + /** + * 邮件 + */ + public final static Pattern EMAIL = PatternPool.EMAIL; + /** + * 移动电话 + */ + public final static Pattern MOBILE = PatternPool.MOBILE; + + /** + * 身份证号码 + */ + public final static Pattern CITIZEN_ID = PatternPool.CITIZEN_ID; + + /** + * 邮编 + */ + public final static Pattern ZIP_CODE = PatternPool.ZIP_CODE; + /** + * 生日 + */ + public final static Pattern BIRTHDAY = PatternPool.BIRTHDAY; + /** + * URL + */ + public final static Pattern URL = PatternPool.URL; + /** + * Http URL + */ + public final static Pattern URL_HTTP = PatternPool.URL_HTTP; + /** + * 中文字、英文字母、数字和下划线 + */ + public final static Pattern GENERAL_WITH_CHINESE = PatternPool.GENERAL_WITH_CHINESE; + /** + * UUID + */ + public final static Pattern UUID = PatternPool.UUID; + /** + * 不带横线的UUID + */ + public final static Pattern UUID_SIMPLE = PatternPool.UUID_SIMPLE; + /** + * 中国车牌号码 + */ + public final static Pattern PLATE_NUMBER = PatternPool.PLATE_NUMBER; + /** + * 车架号;别名:车辆识别代号 车辆识别码;十七位码 + */ + public final static Pattern CAR_VIN = PatternPool.CAR_VIN; + /** + * 驾驶证 别名:驾驶证档案编号、行驶证编号;12位数字字符串;仅限:中国驾驶证档案编号 + */ + public final static Pattern CAR_DRIVING_LICENCE = PatternPool.CAR_DRIVING_LICENCE; + + /** + * 给定值是否为{@code true} + * + * @param value 值 + * @return 是否为true + * @since 4.4.5 + */ + public static boolean isTrue(boolean value) { + return value; + } + + /** + * 给定值是否不为{@code false} + * + * @param value 值 + * @return 是否不为false + * @since 4.4.5 + */ + public static boolean isFalse(boolean value) { + return !value; + } + + /** + * 检查指定值是否为{@code true} + * + * @param value 值 + * @param errorMsgTemplate 错误消息内容模板(变量使用{}表示) + * @param params 模板中变量替换后的值 + * @return 检查过后的值 + * @throws ValidateException 检查不满足条件抛出的异常 + * @since 4.4.5 + */ + public static boolean validateTrue(boolean value, String errorMsgTemplate, Object... params) throws ValidateException { + if (isFalse(value)) { + throw new ValidateException(errorMsgTemplate, params); + } + return true; + } + + /** + * 检查指定值是否为{@code false} + * + * @param value 值 + * @param errorMsgTemplate 错误消息内容模板(变量使用{}表示) + * @param params 模板中变量替换后的值 + * @return 检查过后的值 + * @throws ValidateException 检查不满足条件抛出的异常 + * @since 4.4.5 + */ + public static boolean validateFalse(boolean value, String errorMsgTemplate, Object... params) throws ValidateException { + if (isTrue(value)) { + throw new ValidateException(errorMsgTemplate, params); + } + return false; + } + + /** + * 给定值是否为{@code null} + * + * @param value 值 + * @return 是否为null + */ + public static boolean isNull(Object value) { + return null == value; + } + + /** + * 给定值是否不为{@code null} + * + * @param value 值 + * @return 是否不为null + */ + public static boolean isNotNull(Object value) { + return null != value; + } + + /** + * 检查指定值是否为{@code null} + * + * @param 被检查的对象类型 + * @param value 值 + * @param errorMsgTemplate 错误消息内容模板(变量使用{}表示) + * @param params 模板中变量替换后的值 + * @return 检查过后的值 + * @throws ValidateException 检查不满足条件抛出的异常 + * @since 4.4.5 + */ + public static T validateNull(T value, String errorMsgTemplate, Object... params) throws ValidateException { + if (isNotNull(value)) { + throw new ValidateException(errorMsgTemplate, params); + } + return null; + } + + /** + * 检查指定值是否非{@code null} + * + * @param 被检查的对象类型 + * @param value 值 + * @param errorMsgTemplate 错误消息内容模板(变量使用{}表示) + * @param params 模板中变量替换后的值 + * @return 检查过后的值 + * @throws ValidateException 检查不满足条件抛出的异常 + */ + public static T validateNotNull(T value, String errorMsgTemplate, Object... params) throws ValidateException { + if (isNull(value)) { + throw new ValidateException(errorMsgTemplate, params); + } + return value; + } + + /** + * 验证是否为空
+ * 对于String类型判定是否为empty(null 或 "")
+ * + * @param value 值 + * @return 是否为空 + */ + public static boolean isEmpty(Object value) { + return (null == value || (value instanceof String && StrUtil.isEmpty((String) value))); + } + + /** + * 验证是否为非空
+ * 对于String类型判定是否为empty(null 或 "")
+ * + * @param value 值 + * @return 是否为空 + */ + public static boolean isNotEmpty(Object value) { + return !isEmpty(value); + } + + /** + * 验证是否为空,非空时抛出异常
+ * 对于String类型判定是否为empty(null 或 "")
+ * + * @param 值类型 + * @param value 值 + * @param errorMsg 验证错误的信息 + * @return 验证后的值,验证通过返回此值,空值 + * @throws ValidateException 验证异常 + */ + public static T validateEmpty(T value, String errorMsg) throws ValidateException { + if (isNotEmpty(value)) { + throw new ValidateException(errorMsg); + } + return value; + } + + /** + * 验证是否为非空,为空时抛出异常
+ * 对于String类型判定是否为empty(null 或 "")
+ * + * @param 值类型 + * @param value 值 + * @param errorMsg 验证错误的信息 + * @return 验证后的值,验证通过返回此值,非空值 + * @throws ValidateException 验证异常 + */ + public static T validateNotEmpty(T value, String errorMsg) throws ValidateException { + if (isEmpty(value)) { + throw new ValidateException(errorMsg); + } + return value; + } + + /** + * 验证是否相等
+ * 当两值都为null返回true + * + * @param t1 对象1 + * @param t2 对象2 + * @return 当两值都为null或相等返回true + */ + public static boolean equal(Object t1, Object t2) { + return ObjectUtil.equal(t1, t2); + } + + /** + * 验证是否相等,不相等抛出异常
+ * + * @param t1 对象1 + * @param t2 对象2 + * @param errorMsg 错误信息 + * @return 相同值 + * @throws ValidateException 验证异常 + */ + public static Object validateEqual(Object t1, Object t2, String errorMsg) throws ValidateException { + if (!equal(t1, t2)) { + throw new ValidateException(errorMsg); + } + return t1; + } + + /** + * 验证是否不等,相等抛出异常
+ * + * @param t1 对象1 + * @param t2 对象2 + * @param errorMsg 错误信息 + * @throws ValidateException 验证异常 + */ + public static void validateNotEqual(Object t1, Object t2, String errorMsg) throws ValidateException { + if (equal(t1, t2)) { + throw new ValidateException(errorMsg); + } + } + + /** + * 验证是否非空且与指定值相等
+ * 当数据为空时抛出验证异常
+ * 当两值不等时抛出异常 + * + * @param t1 对象1 + * @param t2 对象2 + * @param errorMsg 错误信息 + * @throws ValidateException 验证异常 + */ + public static void validateNotEmptyAndEqual(Object t1, Object t2, String errorMsg) throws ValidateException { + validateNotEmpty(t1, errorMsg); + validateEqual(t1, t2, errorMsg); + } + + /** + * 验证是否非空且与指定值相等
+ * 当数据为空时抛出验证异常
+ * 当两值相等时抛出异常 + * + * @param t1 对象1 + * @param t2 对象2 + * @param errorMsg 错误信息 + * @throws ValidateException 验证异常 + */ + public static void validateNotEmptyAndNotEqual(Object t1, Object t2, String errorMsg) throws ValidateException { + validateNotEmpty(t1, errorMsg); + validateNotEqual(t1, t2, errorMsg); + } + + /** + * 通过正则表达式验证
+ * 不符合正则抛出{@link ValidateException} 异常 + * + * @param 字符串类型 + * @param regex 正则 + * @param value 值 + * @param errorMsg 验证错误的信息 + * @return 验证后的值 + * @throws ValidateException 验证异常 + */ + public static T validateMatchRegex(String regex, T value, String errorMsg) throws ValidateException { + if (!isMatchRegex(regex, value)) { + throw new ValidateException(errorMsg); + } + return value; + } + + /** + * 通过正则表达式验证 + * + * @param pattern 正则模式 + * @param value 值 + * @return 是否匹配正则 + */ + public static boolean isMatchRegex(Pattern pattern, CharSequence value) { + return ReUtil.isMatch(pattern, value); + } + + /** + * 通过正则表达式验证 + * + * @param regex 正则 + * @param value 值 + * @return 是否匹配正则 + */ + public static boolean isMatchRegex(String regex, CharSequence value) { + return ReUtil.isMatch(regex, value); + } + + /** + * 验证是否为英文字母 、数字和下划线 + * + * @param value 值 + * @return 是否为英文字母 、数字和下划线 + */ + public static boolean isGeneral(CharSequence value) { + return isMatchRegex(GENERAL, value); + } + + /** + * 验证是否为英文字母 、数字和下划线 + * + * @param 字符串类型 + * @param value 值 + * @param errorMsg 验证错误的信息 + * @return 验证后的值 + * @throws ValidateException 验证异常 + */ + public static T validateGeneral(T value, String errorMsg) throws ValidateException { + if (!isGeneral(value)) { + throw new ValidateException(errorMsg); + } + return value; + } + + /** + * 验证是否为给定长度范围的英文字母 、数字和下划线 + * + * @param value 值 + * @param min 最小长度,负数自动识别为0 + * @param max 最大长度,0或负数表示不限制最大长度 + * @return 是否为给定长度范围的英文字母 、数字和下划线 + */ + public static boolean isGeneral(CharSequence value, int min, int max) { + if (min < 0) { + min = 0; + } + String reg = "^\\w{" + min + "," + max + "}$"; + if (max <= 0) { + reg = "^\\w{" + min + ",}$"; + } + return isMatchRegex(reg, value); + } + + /** + * 验证是否为给定长度范围的英文字母 、数字和下划线 + * + * @param 字符串类型 + * @param value 值 + * @param min 最小长度,负数自动识别为0 + * @param max 最大长度,0或负数表示不限制最大长度 + * @param errorMsg 验证错误的信息 + * @return 验证后的值 + * @throws ValidateException 验证异常 + */ + public static T validateGeneral(T value, int min, int max, String errorMsg) throws ValidateException { + if (!isGeneral(value, min, max)) { + throw new ValidateException(errorMsg); + } + return value; + } + + /** + * 验证是否为给定最小长度的英文字母 、数字和下划线 + * + * @param value 值 + * @param min 最小长度,负数自动识别为0 + * @return 是否为给定最小长度的英文字母 、数字和下划线 + */ + public static boolean isGeneral(CharSequence value, int min) { + return isGeneral(value, min, 0); + } + + /** + * 验证是否为给定最小长度的英文字母 、数字和下划线 + * + * @param 字符串类型 + * @param value 值 + * @param min 最小长度,负数自动识别为0 + * @param errorMsg 验证错误的信息 + * @return 验证后的值 + * @throws ValidateException 验证异常 + */ + public static T validateGeneral(T value, int min, String errorMsg) throws ValidateException { + return validateGeneral(value, min, 0, errorMsg); + } + + /** + * 判断字符串是否全部为字母组成,包括大写和小写字母和汉字 + * + * @param value 值 + * @return 是否全部为字母组成,包括大写和小写字母和汉字 + * @since 3.3.0 + */ + public static boolean isLetter(CharSequence value) { + return StrUtil.isAllCharMatch(value, Character::isLetter); + } + + /** + * 验证是否全部为字母组成,包括大写和小写字母和汉字 + * + * @param 字符串类型 + * @param value 表单值 + * @param errorMsg 验证错误的信息 + * @return 验证后的值 + * @throws ValidateException 验证异常 + * @since 3.3.0 + */ + public static T validateLetter(T value, String errorMsg) throws ValidateException { + if (!isLetter(value)) { + throw new ValidateException(errorMsg); + } + return value; + } + + /** + * 判断字符串是否全部为大写字母 + * + * @param value 值 + * @return 是否全部为大写字母 + * @since 3.3.0 + */ + public static boolean isUpperCase(CharSequence value) { + return StrUtil.isAllCharMatch(value, Character::isUpperCase); + } + + /** + * 验证字符串是否全部为大写字母 + * + * @param 字符串类型 + * @param value 表单值 + * @param errorMsg 验证错误的信息 + * @return 验证后的值 + * @throws ValidateException 验证异常 + * @since 3.3.0 + */ + public static T validateUpperCase(T value, String errorMsg) throws ValidateException { + if (!isUpperCase(value)) { + throw new ValidateException(errorMsg); + } + return value; + } + + /** + * 判断字符串是否全部为小写字母 + * + * @param value 值 + * @return 是否全部为小写字母 + * @since 3.3.0 + */ + public static boolean isLowerCase(CharSequence value) { + return StrUtil.isAllCharMatch(value, Character::isLowerCase); + } + + /** + * 验证字符串是否全部为小写字母 + * + * @param 字符串类型 + * @param value 表单值 + * @param errorMsg 验证错误的信息 + * @return 验证后的值 + * @throws ValidateException 验证异常 + * @since 3.3.0 + */ + public static T validateLowerCase(T value, String errorMsg) throws ValidateException { + if (!isLowerCase(value)) { + throw new ValidateException(errorMsg); + } + return value; + } + + /** + * 验证该字符串是否是数字 + * + * @param value 字符串内容 + * @return 是否是数字 + */ + public static boolean isNumber(CharSequence value) { + return NumberUtil.isNumber(value); + } + + /** + * 是否包含数字 + * + * @param value 当前字符串 + * @return boolean 是否存在数字 + * @since 5.6.5 + */ + public static boolean hasNumber(CharSequence value) { + return ReUtil.contains(PatternPool.NUMBERS, value); + } + + /** + * 验证是否为数字 + * + * @param value 表单值 + * @param errorMsg 验证错误的信息 + * @return 验证后的值 + * @throws ValidateException 验证异常 + */ + public static String validateNumber(String value, String errorMsg) throws ValidateException { + if (!isNumber(value)) { + throw new ValidateException(errorMsg); + } + return value; + } + + /** + * 验证该字符串是否是字母(包括大写和小写字母) + * + * @param value 字符串内容 + * @return 是否是字母(包括大写和小写字母) + * @since 4.1.8 + */ + public static boolean isWord(CharSequence value) { + return isMatchRegex(PatternPool.WORD, value); + } + + /** + * 验证是否为字母(包括大写和小写字母) + * + * @param 字符串类型 + * @param value 表单值 + * @param errorMsg 验证错误的信息 + * @return 验证后的值 + * @throws ValidateException 验证异常 + * @since 4.1.8 + */ + public static T validateWord(T value, String errorMsg) throws ValidateException { + if (!isWord(value)) { + throw new ValidateException(errorMsg); + } + return value; + } + + /** + * 验证是否为货币 + * + * @param value 值 + * @return 是否为货币 + */ + public static boolean isMoney(CharSequence value) { + return isMatchRegex(MONEY, value); + } + + /** + * 验证是否为货币 + * + * @param 字符串类型 + * @param value 值 + * @param errorMsg 验证错误的信息 + * @return 验证后的值 + * @throws ValidateException 验证异常 + */ + public static T validateMoney(T value, String errorMsg) throws ValidateException { + if (!isMoney(value)) { + throw new ValidateException(errorMsg); + } + return value; + + } + + /** + * 验证是否为邮政编码(中国) + * + * @param value 值 + * @return 是否为邮政编码(中国) + */ + public static boolean isZipCode(CharSequence value) { + return isMatchRegex(ZIP_CODE, value); + } + + /** + * 验证是否为邮政编码(中国) + * + * @param 字符串类型 + * @param value 表单值 + * @param errorMsg 验证错误的信息 + * @return 验证后的值 + * @throws ValidateException 验证异常 + */ + public static T validateZipCode(T value, String errorMsg) throws ValidateException { + if (!isZipCode(value)) { + throw new ValidateException(errorMsg); + } + return value; + } + + /** + * 验证是否为可用邮箱地址 + * + * @param value 值 + * @return true为可用邮箱地址 + */ + public static boolean isEmail(CharSequence value) { + return isMatchRegex(EMAIL, value); + } + + /** + * 验证是否为可用邮箱地址 + * + * @param 字符串类型 + * @param value 值 + * @param errorMsg 验证错误的信息 + * @return 验证后的值 + * @throws ValidateException 验证异常 + */ + public static T validateEmail(T value, String errorMsg) throws ValidateException { + if (!isEmail(value)) { + throw new ValidateException(errorMsg); + } + return value; + } + + /** + * 验证是否为手机号码(中国) + * + * @param value 值 + * @return 是否为手机号码(中国) + */ + public static boolean isMobile(CharSequence value) { + return isMatchRegex(MOBILE, value); + } + + /** + * 验证是否为手机号码(中国) + * + * @param 字符串类型 + * @param value 值 + * @param errorMsg 验证错误的信息 + * @return 验证后的值 + * @throws ValidateException 验证异常 + */ + public static T validateMobile(T value, String errorMsg) throws ValidateException { + if (!isMobile(value)) { + throw new ValidateException(errorMsg); + } + return value; + } + + /** + * 验证是否为身份证号码(支持18位、15位和港澳台的10位) + * + * @param value 身份证号,支持18位、15位和港澳台的10位 + * @return 是否为有效身份证号码 + */ + public static boolean isCitizenId(CharSequence value) { + return IdcardUtil.isValidCard(String.valueOf(value)); + } + + /** + * 验证是否为身份证号码(支持18位、15位和港澳台的10位) + * + * @param 字符串类型 + * @param value 值 + * @param errorMsg 验证错误的信息 + * @return 验证后的值 + * @throws ValidateException 验证异常 + */ + public static T validateCitizenIdNumber(T value, String errorMsg) throws ValidateException { + if (!isCitizenId(value)) { + throw new ValidateException(errorMsg); + } + return value; + } + + /** + * 验证是否为生日 + * + * @param year 年,从1900年开始计算 + * @param month 月,从1开始计数 + * @param day 日,从1开始计数 + * @return 是否为生日 + */ + public static boolean isBirthday(int year, int month, int day) { + // 验证年 + int thisYear = DateUtil.thisYear(); + if (year < 1900 || year > thisYear) { + return false; + } + + // 验证月 + if (month < 1 || month > 12) { + return false; + } + + // 验证日 + if (day < 1 || day > 31) { + return false; + } + // 检查几个特殊月的最大天数 + if (day == 31 && (month == 4 || month == 6 || month == 9 || month == 11)) { + return false; + } + if (month == 2) { + // 在2月,非闰年最大28,闰年最大29 + return day < 29 || (day == 29 && DateUtil.isLeapYear(year)); + } + return true; + } + + /** + * 验证是否为生日
+ * 只支持以下几种格式: + *

    + *
  • yyyyMMdd
  • + *
  • yyyy-MM-dd
  • + *
  • yyyy/MM/dd
  • + *
  • yyyy.MM.dd
  • + *
  • yyyy年MM月dd日
  • + *
+ * + * @param value 值 + * @return 是否为生日 + */ + public static boolean isBirthday(CharSequence value) { + final Matcher matcher = BIRTHDAY.matcher(value); + if (matcher.find()) { + int year = Integer.parseInt(matcher.group(1)); + int month = Integer.parseInt(matcher.group(3)); + int day = Integer.parseInt(matcher.group(5)); + return isBirthday(year, month, day); + } + return false; + } + + /** + * 验证验证是否为生日 + * + * @param 字符串类型 + * @param value 值 + * @param errorMsg 验证错误的信息 + * @return 验证后的值 + * @throws ValidateException 验证异常 + */ + public static T validateBirthday(T value, String errorMsg) throws ValidateException { + if (!isBirthday(value)) { + throw new ValidateException(errorMsg); + } + return value; + } + + /** + * 验证是否为IPV4地址 + * + * @param value 值 + * @return 是否为IPV4地址 + */ + public static boolean isIpv4(CharSequence value) { + return isMatchRegex(IPV4, value); + } + + /** + * 验证是否为IPV4地址 + * + * @param 字符串类型 + * @param value 值 + * @param errorMsg 验证错误的信息 + * @return 验证后的值 + * @throws ValidateException 验证异常 + */ + public static T validateIpv4(T value, String errorMsg) throws ValidateException { + if (!isIpv4(value)) { + throw new ValidateException(errorMsg); + } + return value; + } + + /** + * 验证是否为IPV6地址 + * + * @param value 值 + * @return 是否为IPV6地址 + */ + public static boolean isIpv6(CharSequence value) { + return isMatchRegex(IPV6, value); + } + + /** + * 验证是否为IPV6地址 + * + * @param 字符串类型 + * @param value 值 + * @param errorMsg 验证错误的信息 + * @return 验证后的值 + * @throws ValidateException 验证异常 + */ + public static T validateIpv6(T value, String errorMsg) throws ValidateException { + if (!isIpv6(value)) { + throw new ValidateException(errorMsg); + } + return value; + } + + /** + * 验证是否为MAC地址 + * + * @param value 值 + * @return 是否为MAC地址 + * @since 4.1.3 + */ + public static boolean isMac(CharSequence value) { + return isMatchRegex(PatternPool.MAC_ADDRESS, value); + } + + /** + * 验证是否为MAC地址 + * + * @param 字符串类型 + * @param value 值 + * @param errorMsg 验证错误的信息 + * @return 验证后的值 + * @throws ValidateException 验证异常 + * @since 4.1.3 + */ + public static T validateMac(T value, String errorMsg) throws ValidateException { + if (!isMac(value)) { + throw new ValidateException(errorMsg); + } + return value; + } + + /** + * 验证是否为中国车牌号 + * + * @param value 值 + * @return 是否为中国车牌号 + * @since 3.0.6 + */ + public static boolean isPlateNumber(CharSequence value) { + return isMatchRegex(PLATE_NUMBER, value); + } + + /** + * 验证是否为中国车牌号 + * + * @param 字符串类型 + * @param value 值 + * @param errorMsg 验证错误的信息 + * @return 验证后的值 + * @throws ValidateException 验证异常 + * @since 3.0.6 + */ + public static T validatePlateNumber(T value, String errorMsg) throws ValidateException { + if (!isPlateNumber(value)) { + throw new ValidateException(errorMsg); + } + return value; + } + + /** + * 验证是否为URL + * + * @param value 值 + * @return 是否为URL + */ + public static boolean isUrl(CharSequence value) { + if (StrUtil.isBlank(value)) { + return false; + } + try { + new java.net.URL(StrUtil.str(value)); + } catch (MalformedURLException e) { + return false; + } + return true; + } + + /** + * 验证是否为URL + * + * @param 字符串类型 + * @param value 值 + * @param errorMsg 验证错误的信息 + * @return 验证后的值 + * @throws ValidateException 验证异常 + */ + public static T validateUrl(T value, String errorMsg) throws ValidateException { + if (!isUrl(value)) { + throw new ValidateException(errorMsg); + } + return value; + } + + /** + * 验证是否都为汉字 + * + * @param value 值 + * @return 是否为汉字 + */ + public static boolean isChinese(CharSequence value) { + return isMatchRegex(PatternPool.CHINESES, value); + } + + /** + * 验证是否包含汉字 + * + * @param value 值 + * @return 是否包含汉字 + * @since 5.2.1 + */ + public static boolean hasChinese(CharSequence value) { + return ReUtil.contains(ReUtil.RE_CHINESES, value); + } + + /** + * 验证是否为汉字 + * + * @param 字符串类型 + * @param value 表单值 + * @param errorMsg 验证错误的信息 + * @return 验证后的值 + * @throws ValidateException 验证异常 + */ + public static T validateChinese(T value, String errorMsg) throws ValidateException { + if (!isChinese(value)) { + throw new ValidateException(errorMsg); + } + return value; + } + + /** + * 验证是否为中文字、英文字母、数字和下划线 + * + * @param value 值 + * @return 是否为中文字、英文字母、数字和下划线 + */ + public static boolean isGeneralWithChinese(CharSequence value) { + return isMatchRegex(GENERAL_WITH_CHINESE, value); + } + + /** + * 验证是否为中文字、英文字母、数字和下划线 + * + * @param 字符串类型 + * @param value 值 + * @param errorMsg 验证错误的信息 + * @return 验证后的值 + * @throws ValidateException 验证异常 + */ + public static T validateGeneralWithChinese(T value, String errorMsg) throws ValidateException { + if (!isGeneralWithChinese(value)) { + throw new ValidateException(errorMsg); + } + return value; + } + + /** + * 验证是否为UUID
+ * 包括带横线标准格式和不带横线的简单模式 + * + * @param value 值 + * @return 是否为UUID + */ + public static boolean isUUID(CharSequence value) { + return isMatchRegex(UUID, value) || isMatchRegex(UUID_SIMPLE, value); + } + + /** + * 验证是否为UUID
+ * 包括带横线标准格式和不带横线的简单模式 + * + * @param 字符串类型 + * @param value 值 + * @param errorMsg 验证错误的信息 + * @return 验证后的值 + * @throws ValidateException 验证异常 + */ + public static T validateUUID(T value, String errorMsg) throws ValidateException { + if (!isUUID(value)) { + throw new ValidateException(errorMsg); + } + return value; + } + + /** + * 验证是否为Hex(16进制)字符串 + * + * @param value 值 + * @return 是否为Hex(16进制)字符串 + * @since 4.3.3 + */ + public static boolean isHex(CharSequence value) { + return isMatchRegex(PatternPool.HEX, value); + } + + /** + * 验证是否为Hex(16进制)字符串 + * + * @param 字符串类型 + * @param value 值 + * @param errorMsg 验证错误的信息 + * @return 验证后的值 + * @throws ValidateException 验证异常 + * @since 4.3.3 + */ + public static T validateHex(T value, String errorMsg) throws ValidateException { + if (!isHex(value)) { + throw new ValidateException(errorMsg); + } + return value; + } + + /** + * 检查给定的数字是否在指定范围内 + * + * @param value 值 + * @param min 最小值(包含) + * @param max 最大值(包含) + * @return 是否满足 + * @since 4.1.10 + */ + public static boolean isBetween(Number value, Number min, Number max) { + Assert.notNull(value); + Assert.notNull(min); + Assert.notNull(max); + final double doubleValue = value.doubleValue(); + return (doubleValue >= min.doubleValue()) && (doubleValue <= max.doubleValue()); + } + + /** + * 检查给定的数字是否在指定范围内 + * + * @param value 值 + * @param min 最小值(包含) + * @param max 最大值(包含) + * @param errorMsg 验证错误的信息 + * @throws ValidateException 验证异常 + * @since 4.1.10 + */ + public static void validateBetween(Number value, Number min, Number max, String errorMsg) throws ValidateException { + if (!isBetween(value, min, max)) { + throw new ValidateException(errorMsg); + } + } + + /** + * 是否是有效的统一社会信用代码 + *
+	 * 第一部分:登记管理部门代码1位 (数字或大写英文字母)
+	 * 第二部分:机构类别代码1位 (数字或大写英文字母)
+	 * 第三部分:登记管理机关行政区划码6位 (数字)
+	 * 第四部分:主体标识码(组织机构代码)9位 (数字或大写英文字母)
+	 * 第五部分:校验码1位 (数字或大写英文字母)
+	 * 
+ * + * @param creditCode 统一社会信用代码 + * @return 校验结果 + * @since 5.2.4 + */ + public static boolean isCreditCode(CharSequence creditCode) { + return CreditCodeUtil.isCreditCode(creditCode); + } + + /** + * 验证是否为车架号;别名:行驶证编号 车辆识别代号 车辆识别码 + * + * @param value 值,17位车架号;形如:LSJA24U62JG269225、LDC613P23A1305189 + * @return 是否为车架号 + * @author dazer and ourslook + * @since 5.6.3 + */ + public static boolean isCarVin(CharSequence value) { + return isMatchRegex(CAR_VIN, value); + } + + /** + * 验证是否为车架号;别名:行驶证编号 车辆识别代号 车辆识别码 + * + * @param 字符串类型 + * @param value 值 + * @param errorMsg 验证错误的信息 + * @return 验证后的值 + * @throws ValidateException 验证异常 + * @author dazer and ourslook + * @since 5.6.3 + */ + public static T validateCarVin(T value, String errorMsg) throws ValidateException { + if (!isCarVin(value)) { + throw new ValidateException(errorMsg); + } + return value; + } + + /** + * 验证是否为驾驶证 别名:驾驶证档案编号、行驶证编号 + * 仅限:中国驾驶证档案编号 + * + * @param value 值,12位数字字符串,eg:430101758218 + * @return 是否为档案编号 + * @author dazer and ourslook + * @since 5.6.3 + */ + public static boolean isCarDrivingLicence(CharSequence value) { + return isMatchRegex(CAR_DRIVING_LICENCE, value); + } + + + /** + * 是否是中文姓名 + * 维吾尔族姓名里面的点是 · 输入法中文状态下,键盘左上角数字1前面的那个符号;
+ * 错误字符:{@code ..。..}
+ * 正确维吾尔族姓名: + *
+	 * 霍加阿卜杜拉·麦提喀斯木
+	 * 玛合萨提别克·哈斯木别克
+	 * 阿布都热依木江·艾斯卡尔
+	 * 阿卜杜尼亚孜·毛力尼亚孜
+	 * 
+ *
+	 * ----------
+	 * 错误示例:孟  伟                reason: 有空格
+	 * 错误示例:连逍遥0               reason: 数字
+	 * 错误示例:依帕古丽-艾则孜        reason: 特殊符号
+	 * 错误示例:牙力空.买提萨力        reason: 新疆人的点不对
+	 * 错误示例:王建鹏2002-3-2        reason: 有数字、特殊符号
+	 * 错误示例:雷金默(雷皓添)        reason: 有括号
+	 * 错误示例:翟冬:亮               reason: 有特殊符号
+	 * 错误示例:李                   reason: 少于2位
+	 * ----------
+	 * 
+ * 总结中文姓名:2-60位,只能是中文和 · + * + * @param value 中文姓名 + * @return 是否是正确的中文姓名 + * @author dazer + * @since 5.8.0.M3 + */ + public static boolean isChineseName(CharSequence value) { + return isMatchRegex(PatternPool.CHINESE_NAME, value); + } + + + /** + * 验证是否为驾驶证 别名:驾驶证档案编号、行驶证编号 + * + * @param 字符串类型 + * @param value 值 + * @param errorMsg 验证错误的信息 + * @return 验证后的值 + * @throws ValidateException 验证异常 + * @author dazer and ourslook + * @since 5.6.3 + */ + public static T validateCarDrivingLicence(T value, String errorMsg) throws ValidateException { + if (!isCarDrivingLicence(value)) { + throw new ValidateException(errorMsg); + } + return value; + } +} diff --git a/src/main/java/cn/hutool/core/lang/WeightRandom.java b/src/main/java/cn/hutool/core/lang/WeightRandom.java new file mode 100644 index 0000000..e6b7b33 --- /dev/null +++ b/src/main/java/cn/hutool/core/lang/WeightRandom.java @@ -0,0 +1,235 @@ +package cn.hutool.core.lang; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.RandomUtil; + +import java.io.Serializable; +import java.util.Random; +import java.util.SortedMap; +import java.util.TreeMap; + +/** + * 权重随机算法实现
+ *

+ * 平时,经常会遇到权重随机算法,从不同权重的N个元素中随机选择一个,并使得总体选择结果是按照权重分布的。如广告投放、负载均衡等。 + *

+ *

+ * 如有4个元素A、B、C、D,权重分别为1、2、3、4,随机结果中A:B:C:D的比例要为1:2:3:4。
+ *

+ * 总体思路:累加每个元素的权重A(1)-B(3)-C(6)-D(10),则4个元素的的权重管辖区间分别为[0,1)、[1,3)、[3,6)、[6,10)。
+ * 然后随机出一个[0,10)之间的随机数。落在哪个区间,则该区间之后的元素即为按权重命中的元素。
+ * + *

+ * 参考博客:https://www.cnblogs.com/waterystone/p/5708063.html + *

+ * + * @param 权重随机获取的对象类型 + * @author looly + * @since 3.3.0 + */ +public class WeightRandom implements Serializable { + private static final long serialVersionUID = -8244697995702786499L; + + private final TreeMap weightMap; + + + /** + * 创建权重随机获取器 + * + * @param 权重随机获取的对象类型 + * @return {@link WeightRandom} + */ + public static WeightRandom create() { + return new WeightRandom<>(); + } + + // ---------------------------------------------------------------------------------- Constructor start + /** + * 构造 + */ + public WeightRandom() { + weightMap = new TreeMap<>(); + + } + + /** + * 构造 + * + * @param weightObj 带有权重的对象 + */ + public WeightRandom(WeightObj weightObj) { + this(); + if(null != weightObj) { + add(weightObj); + } + } + + /** + * 构造 + * + * @param weightObjs 带有权重的对象 + */ + public WeightRandom(Iterable> weightObjs) { + this(); + if(CollUtil.isNotEmpty(weightObjs)) { + for (WeightObj weightObj : weightObjs) { + add(weightObj); + } + } + } + + /** + * 构造 + * + * @param weightObjs 带有权重的对象 + */ + public WeightRandom(WeightObj[] weightObjs) { + this(); + for (WeightObj weightObj : weightObjs) { + add(weightObj); + } + } + // ---------------------------------------------------------------------------------- Constructor end + + /** + * 增加对象 + * + * @param obj 对象 + * @param weight 权重 + * @return this + */ + public WeightRandom add(T obj, double weight) { + return add(new WeightObj<>(obj, weight)); + } + + /** + * 增加对象权重 + * + * @param weightObj 权重对象 + * @return this + */ + public WeightRandom add(WeightObj weightObj) { + if(null != weightObj) { + final double weight = weightObj.getWeight(); + if(weightObj.getWeight() > 0) { + double lastWeight = (this.weightMap.size() == 0) ? 0 : this.weightMap.lastKey(); + this.weightMap.put(weight + lastWeight, weightObj.getObj());// 权重累加 + } + } + return this; + } + + /** + * 清空权重表 + * + * @return this + */ + public WeightRandom clear() { + if(null != this.weightMap) { + this.weightMap.clear(); + } + return this; + } + + /** + * 下一个随机对象 + * + * @return 随机对象 + */ + public T next() { + if(MapUtil.isEmpty(this.weightMap)) { + return null; + } + final Random random = RandomUtil.getRandom(); + final double randomWeight = this.weightMap.lastKey() * random.nextDouble(); + final SortedMap tailMap = this.weightMap.tailMap(randomWeight, false); + return this.weightMap.get(tailMap.firstKey()); + } + + /** + * 带有权重的对象包装 + * + * @author looly + * + * @param 对象类型 + */ + public static class WeightObj { + /** 对象 */ + private T obj; + /** 权重 */ + private final double weight; + + /** + * 构造 + * + * @param obj 对象 + * @param weight 权重 + */ + public WeightObj(T obj, double weight) { + this.obj = obj; + this.weight = weight; + } + + /** + * 获取对象 + * + * @return 对象 + */ + public T getObj() { + return obj; + } + + /** + * 设置对象 + * + * @param obj 对象 + */ + public void setObj(T obj) { + this.obj = obj; + } + + /** + * 获取权重 + * + * @return 权重 + */ + public double getWeight() { + return weight; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((obj == null) ? 0 : obj.hashCode()); + long temp; + temp = Double.doubleToLongBits(weight); + result = prime * result + (int) (temp ^ (temp >>> 32)); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + WeightObj other = (WeightObj) obj; + if (this.obj == null) { + if (other.obj != null) { + return false; + } + } else if (!this.obj.equals(other.obj)) { + return false; + } + return Double.doubleToLongBits(weight) == Double.doubleToLongBits(other.weight); + } + } + +} diff --git a/src/main/java/cn/hutool/core/lang/ansi/Ansi8BitColor.java b/src/main/java/cn/hutool/core/lang/ansi/Ansi8BitColor.java new file mode 100644 index 0000000..7de3a92 --- /dev/null +++ b/src/main/java/cn/hutool/core/lang/ansi/Ansi8BitColor.java @@ -0,0 +1,93 @@ +package cn.hutool.core.lang.ansi; + +import cn.hutool.core.lang.Assert; + +/** + * ANSI 8-bit前景或背景色(即8位编码,共256种颜色(2^8) )
+ *

    + *
  • 0-7: 标准颜色(同ESC [ 30–37 m)
  • + *
  • 8-15: 高强度颜色(同ESC [ 90–97 m)
  • + *
  • 16-231(6 × 6 × 6 共 216色): 16 + 36 × r + 6 × g + b (0 ≤ r, g, b ≤ 5)
  • + *
  • 232-255: 从黑到白的24阶灰度色
  • + *
+ * + *

来自Spring Boot

+ * + * @author Toshiaki Maki, Phillip Webb + * @see #foreground(int) + * @see #background(int) + * @since 5.8.0 + */ +public final class Ansi8BitColor implements AnsiElement { + + private static final String PREFIX_FORE = "38;5;"; + private static final String PREFIX_BACK = "48;5;"; + + /** + * 前景色ANSI颜色实例 + * + * @param code 颜色代码(0-255) + * @return 前景色ANSI颜色实例 + */ + public static Ansi8BitColor foreground(int code) { + return new Ansi8BitColor(PREFIX_FORE, code); + } + + /** + * 背景色ANSI颜色实例 + * + * @param code 颜色代码(0-255) + * @return 背景色ANSI颜色实例 + */ + public static Ansi8BitColor background(int code) { + return new Ansi8BitColor(PREFIX_BACK, code); + } + + private final String prefix; + private final int code; + + /** + * 构造 + * + * @param prefix 前缀 + * @param code 颜色代码(0-255) + * @throws IllegalArgumentException 颜色代码不在0~255范围内 + */ + private Ansi8BitColor(String prefix, int code) { + Assert.isTrue(code >= 0 && code <= 255, "Code must be between 0 and 255"); + this.prefix = prefix; + this.code = code; + } + + /** + * 获取颜色代码(0-255) + * + * @return 颜色代码(0 - 255) + */ + @Override + public int getCode() { + return this.code; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + Ansi8BitColor other = (Ansi8BitColor) obj; + return this.prefix.equals(other.prefix) && this.code == other.code; + } + + @Override + public int hashCode() { + return this.prefix.hashCode() * 31 + this.code; + } + + @Override + public String toString() { + return this.prefix + this.code; + } +} diff --git a/src/main/java/cn/hutool/core/lang/ansi/AnsiBackground.java b/src/main/java/cn/hutool/core/lang/ansi/AnsiBackground.java new file mode 100644 index 0000000..93550eb --- /dev/null +++ b/src/main/java/cn/hutool/core/lang/ansi/AnsiBackground.java @@ -0,0 +1,121 @@ +package cn.hutool.core.lang.ansi; + +import cn.hutool.core.util.StrUtil; + +/** + * ANSI背景颜色枚举 + * + *

来自Spring Boot

+ * + * @author Phillip Webb, Geoffrey Chandler + * @since 5.8.0 + */ +public enum AnsiBackground implements AnsiElement { + + /** + * 默认背景色 + */ + DEFAULT(49), + + /** + * 黑色 + */ + BLACK(40), + + /** + * 红 + */ + RED(41), + + /** + * 绿 + */ + GREEN(42), + + /** + * 黄 + */ + YELLOW(43), + + /** + * 蓝 + */ + BLUE(44), + + /** + * 品红 + */ + MAGENTA(45), + + /** + * 青 + */ + CYAN(46), + + /** + * 白 + */ + WHITE(47), + + /** + * 亮黑 + */ + BRIGHT_BLACK(100), + + /** + * 亮红 + */ + BRIGHT_RED(101), + + /** + * 亮绿 + */ + BRIGHT_GREEN(102), + + /** + * 亮黄 + */ + BRIGHT_YELLOW(103), + + /** + * 亮蓝 + */ + BRIGHT_BLUE(104), + + /** + * 亮品红 + */ + BRIGHT_MAGENTA(105), + + /** + * 亮青 + */ + BRIGHT_CYAN(106), + + /** + * 亮白 + */ + BRIGHT_WHITE(107); + + private final int code; + + AnsiBackground(int code) { + this.code = code; + } + + /** + * 获取ANSI颜色代码 + * + * @return 颜色代码 + */ + @Override + public int getCode() { + return this.code; + } + + @Override + public String toString() { + return StrUtil.toString(this.code); + } + +} diff --git a/src/main/java/cn/hutool/core/lang/ansi/AnsiColor.java b/src/main/java/cn/hutool/core/lang/ansi/AnsiColor.java new file mode 100644 index 0000000..0af3543 --- /dev/null +++ b/src/main/java/cn/hutool/core/lang/ansi/AnsiColor.java @@ -0,0 +1,121 @@ +package cn.hutool.core.lang.ansi; + +import cn.hutool.core.util.StrUtil; + +/** + * ANSI标准颜色 + * + *

来自Spring Boot

+ * + * @author Phillip Webb, Geoffrey Chandler + * @since 5.8.0 + */ +public enum AnsiColor implements AnsiElement { + + /** + * 默认前景色 + */ + DEFAULT(39), + + /** + * 黑 + */ + BLACK(30), + + /** + * 红 + */ + RED(31), + + /** + * 绿 + */ + GREEN(32), + + /** + * 黄 + */ + YELLOW(33), + + /** + * 蓝 + */ + BLUE(34), + + /** + * 品红 + */ + MAGENTA(35), + + /** + * 青 + */ + CYAN(36), + + /** + * 白 + */ + WHITE(37), + + /** + * 亮黑 + */ + BRIGHT_BLACK(90), + + /** + * 亮红 + */ + BRIGHT_RED(91), + + /** + * 亮绿 + */ + BRIGHT_GREEN(92), + + /** + * 亮黄 + */ + BRIGHT_YELLOW(93), + + /** + * 亮蓝 + */ + BRIGHT_BLUE(94), + + /** + * 亮品红 + */ + BRIGHT_MAGENTA(95), + + /** + * 亮青 + */ + BRIGHT_CYAN(96), + + /** + * 亮白 + */ + BRIGHT_WHITE(97); + + private final int code; + + AnsiColor(int code) { + this.code = code; + } + + /** + * 获取ANSI颜色代码 + * + * @return 颜色代码 + */ + @Override + public int getCode() { + return this.code; + } + + @Override + public String toString() { + return StrUtil.toString(this.code); + } + +} diff --git a/src/main/java/cn/hutool/core/lang/ansi/AnsiElement.java b/src/main/java/cn/hutool/core/lang/ansi/AnsiElement.java new file mode 100644 index 0000000..a2c6160 --- /dev/null +++ b/src/main/java/cn/hutool/core/lang/ansi/AnsiElement.java @@ -0,0 +1,26 @@ +package cn.hutool.core.lang.ansi; + +/** + * ANSI可转义节点接口,实现为ANSI颜色等 + * + *

来自Spring Boot

+ * + * @author Phillip Webb + */ +public interface AnsiElement { + + /** + * @return ANSI转义编码 + */ + @Override + String toString(); + + /** + * 获取ANSI代码,默认返回-1 + * @return ANSI代码 + * @since 5.8.7 + */ + default int getCode(){ + return -1; + } +} diff --git a/src/main/java/cn/hutool/core/lang/ansi/AnsiEncoder.java b/src/main/java/cn/hutool/core/lang/ansi/AnsiEncoder.java new file mode 100644 index 0000000..5a9dc5f --- /dev/null +++ b/src/main/java/cn/hutool/core/lang/ansi/AnsiEncoder.java @@ -0,0 +1,65 @@ +package cn.hutool.core.lang.ansi; + +/** + * 生成ANSI格式的编码输出 + * + * @author Phillip Webb + * @since 1.0.0 + */ +public abstract class AnsiEncoder { + + private static final String ENCODE_JOIN = ";"; + private static final String ENCODE_START = "\033["; + private static final String ENCODE_END = "m"; + private static final String RESET = "0;" + AnsiColor.DEFAULT; + + /** + * 创建ANSI字符串,参数中的{@link AnsiElement}会被转换为编码形式。 + * + * @param elements 节点数组 + * @return ANSI字符串 + */ + public static String encode(Object... elements) { + final StringBuilder sb = new StringBuilder(); + buildEnabled(sb, elements); + return sb.toString(); + } + + /** + * 追加需要需转义的节点 + * + * @param sb {@link StringBuilder} + * @param elements 节点列表 + */ + private static void buildEnabled(StringBuilder sb, Object[] elements) { + boolean writingAnsi = false; + boolean containsEncoding = false; + for (Object element : elements) { + if (null == element) { + continue; + } + if (element instanceof AnsiElement) { + containsEncoding = true; + if (writingAnsi) { + sb.append(ENCODE_JOIN); + } else { + sb.append(ENCODE_START); + writingAnsi = true; + } + } else { + if (writingAnsi) { + sb.append(ENCODE_END); + writingAnsi = false; + } + } + sb.append(element); + } + + // 恢复默认 + if (containsEncoding) { + sb.append(writingAnsi ? ENCODE_JOIN : ENCODE_START); + sb.append(RESET); + sb.append(ENCODE_END); + } + } +} diff --git a/src/main/java/cn/hutool/core/lang/ansi/AnsiStyle.java b/src/main/java/cn/hutool/core/lang/ansi/AnsiStyle.java new file mode 100644 index 0000000..e92512b --- /dev/null +++ b/src/main/java/cn/hutool/core/lang/ansi/AnsiStyle.java @@ -0,0 +1,61 @@ +package cn.hutool.core.lang.ansi; + +import cn.hutool.core.util.StrUtil; + +/** + * ANSI文本样式风格枚举 + * + *

来自Spring Boot

+ * + * @author Phillip Webb + * @since 5.8.0 + */ +public enum AnsiStyle implements AnsiElement { + + /** + * 重置/正常 + */ + NORMAL(0), + + /** + * 粗体或增加强度 + */ + BOLD(1), + + /** + * 弱化(降低强度) + */ + FAINT(2), + + /** + * 斜体 + */ + ITALIC(3), + + /** + * 下划线 + */ + UNDERLINE(4); + + private final int code; + + AnsiStyle(int code) { + this.code = code; + } + + /** + * 获取ANSI文本样式风格代码 + * + * @return 文本样式风格代码 + */ + @Override + public int getCode() { + return this.code; + } + + @Override + public String toString() { + return StrUtil.toString(this.code); + } + +} diff --git a/src/main/java/cn/hutool/core/lang/ansi/ForeOrBack.java b/src/main/java/cn/hutool/core/lang/ansi/ForeOrBack.java new file mode 100644 index 0000000..518782b --- /dev/null +++ b/src/main/java/cn/hutool/core/lang/ansi/ForeOrBack.java @@ -0,0 +1,16 @@ +package cn.hutool.core.lang.ansi; + +/** + * 区分前景还是背景 + */ +public enum ForeOrBack{ + + /** + * 前景 + */ + FORE, + /** + * 背景 + */ + BACK, +} diff --git a/src/main/java/cn/hutool/core/lang/ansi/package-info.java b/src/main/java/cn/hutool/core/lang/ansi/package-info.java new file mode 100644 index 0000000..e1a6f4d --- /dev/null +++ b/src/main/java/cn/hutool/core/lang/ansi/package-info.java @@ -0,0 +1,6 @@ +/** + * 命令行终端中ANSI 转义序列相关封装,如ANSI颜色等 + * + * @author spring, looly + */ +package cn.hutool.core.lang.ansi; diff --git a/src/main/java/cn/hutool/core/lang/caller/Caller.java b/src/main/java/cn/hutool/core/lang/caller/Caller.java new file mode 100644 index 0000000..c372e2c --- /dev/null +++ b/src/main/java/cn/hutool/core/lang/caller/Caller.java @@ -0,0 +1,47 @@ +package cn.hutool.core.lang.caller; + +/** + * 调用者接口
+ * 可以通过此接口的实现类方法获取调用者、多级调用者以及判断是否被调用 + * + * @author Looly + * + */ +public interface Caller { + /** + * 获得调用者 + * + * @return 调用者 + */ + Class getCaller(); + + /** + * 获得调用者的调用者 + * + * @return 调用者的调用者 + */ + Class getCallerCaller(); + + /** + * 获得调用者,指定第几级调用者 调用者层级关系: + * + *
+	 * 0 {@link CallerUtil}
+	 * 1 调用{@link CallerUtil}中方法的类
+	 * 2 调用者的调用者
+	 * ...
+	 * 
+ * + * @param depth 层级。0表示{@link CallerUtil}本身,1表示调用{@link CallerUtil}的类,2表示调用者的调用者,依次类推 + * @return 第几级调用者 + */ + Class getCaller(int depth); + + /** + * 是否被指定类调用 + * + * @param clazz 调用者类 + * @return 是否被调用 + */ + boolean isCalledBy(Class clazz); +} diff --git a/src/main/java/cn/hutool/core/lang/caller/CallerUtil.java b/src/main/java/cn/hutool/core/lang/caller/CallerUtil.java new file mode 100644 index 0000000..11285d7 --- /dev/null +++ b/src/main/java/cn/hutool/core/lang/caller/CallerUtil.java @@ -0,0 +1,98 @@ +package cn.hutool.core.lang.caller; + +/** + * 调用者。可以通过此类的方法获取调用者、多级调用者以及判断是否被调用 + * + * @author Looly + * @since 4.1.6 + */ +public class CallerUtil { + private static final Caller INSTANCE; + static { + INSTANCE = tryCreateCaller(); + } + + /** + * 获得调用者 + * + * @return 调用者 + */ + public static Class getCaller() { + return INSTANCE.getCaller(); + } + + /** + * 获得调用者的调用者 + * + * @return 调用者的调用者 + */ + public static Class getCallerCaller() { + return INSTANCE.getCallerCaller(); + } + + /** + * 获得调用者,指定第几级调用者
+ * 调用者层级关系: + * + *
+	 * 0 CallerUtil
+	 * 1 调用CallerUtil中方法的类
+	 * 2 调用者的调用者
+	 * ...
+	 * 
+ * + * @param depth 层级。0表示CallerUtil本身,1表示调用CallerUtil的类,2表示调用者的调用者,依次类推 + * @return 第几级调用者 + */ + public static Class getCaller(int depth) { + return INSTANCE.getCaller(depth); + } + + /** + * 是否被指定类调用 + * + * @param clazz 调用者类 + * @return 是否被调用 + */ + public static boolean isCalledBy(Class clazz) { + return INSTANCE.isCalledBy(clazz); + } + + /** + * 获取调用此方法的方法名 + * + * @param isFullName 是否返回全名,全名包括方法所在类的全路径名 + * @return 调用此方法的方法名 + * @since 5.2.4 + */ + public static String getCallerMethodName(boolean isFullName){ + final StackTraceElement stackTraceElement = Thread.currentThread().getStackTrace()[2]; + final String methodName = stackTraceElement.getMethodName(); + if(!isFullName){ + return methodName; + } + + return stackTraceElement.getClassName() + "." + methodName; + } + + /** + * 尝试创建{@link Caller}实现 + * + * @return {@link Caller}实现 + */ + private static Caller tryCreateCaller() { + Caller caller; + try { + caller = new SecurityManagerCaller(); + if(null != caller.getCaller() && null != caller.getCallerCaller()) { + return caller; + } + } catch (Throwable e) { + //ignore + } + + caller = new StackTraceCaller(); + return caller; + } + // ---------------------------------------------------------------------------------------------- static interface and class +} diff --git a/src/main/java/cn/hutool/core/lang/caller/SecurityManagerCaller.java b/src/main/java/cn/hutool/core/lang/caller/SecurityManagerCaller.java new file mode 100644 index 0000000..7202475 --- /dev/null +++ b/src/main/java/cn/hutool/core/lang/caller/SecurityManagerCaller.java @@ -0,0 +1,56 @@ +package cn.hutool.core.lang.caller; + +import java.io.Serializable; + +import cn.hutool.core.util.ArrayUtil; + +/** + * {@link SecurityManager} 方式获取调用者 + * + * @author Looly + */ +public class SecurityManagerCaller extends SecurityManager implements Caller, Serializable { + private static final long serialVersionUID = 1L; + + private static final int OFFSET = 1; + + @Override + public Class getCaller() { + final Class[] context = getClassContext(); + if (null != context && (OFFSET + 1) < context.length) { + return context[OFFSET + 1]; + } + return null; + } + + @Override + public Class getCallerCaller() { + final Class[] context = getClassContext(); + if (null != context && (OFFSET + 2) < context.length) { + return context[OFFSET + 2]; + } + return null; + } + + @Override + public Class getCaller(int depth) { + final Class[] context = getClassContext(); + if (null != context && (OFFSET + depth) < context.length) { + return context[OFFSET + depth]; + } + return null; + } + + @Override + public boolean isCalledBy(Class clazz) { + final Class[] classes = getClassContext(); + if(ArrayUtil.isNotEmpty(classes)) { + for (Class contextClass : classes) { + if (contextClass.equals(clazz)) { + return true; + } + } + } + return false; + } +} diff --git a/src/main/java/cn/hutool/core/lang/caller/StackTraceCaller.java b/src/main/java/cn/hutool/core/lang/caller/StackTraceCaller.java new file mode 100644 index 0000000..45a5f84 --- /dev/null +++ b/src/main/java/cn/hutool/core/lang/caller/StackTraceCaller.java @@ -0,0 +1,68 @@ +package cn.hutool.core.lang.caller; + +import java.io.Serializable; + +import cn.hutool.core.exceptions.UtilException; + +/** + * 通过StackTrace方式获取调用者。此方式效率最低,不推荐使用 + * + * @author Looly + */ +public class StackTraceCaller implements Caller, Serializable { + private static final long serialVersionUID = 1L; + private static final int OFFSET = 2; + + @Override + public Class getCaller() { + final StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace(); + if (OFFSET + 1 >= stackTrace.length) { + return null; + } + final String className = stackTrace[OFFSET + 1].getClassName(); + try { + return Class.forName(className); + } catch (ClassNotFoundException e) { + throw new UtilException(e, "[{}] not found!", className); + } + } + + @Override + public Class getCallerCaller() { + final StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace(); + if (OFFSET + 2 >= stackTrace.length) { + return null; + } + final String className = stackTrace[OFFSET + 2].getClassName(); + try { + return Class.forName(className); + } catch (ClassNotFoundException e) { + throw new UtilException(e, "[{}] not found!", className); + } + } + + @Override + public Class getCaller(int depth) { + final StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace(); + if (OFFSET + depth >= stackTrace.length) { + return null; + } + final String className = stackTrace[OFFSET + depth].getClassName(); + try { + return Class.forName(className); + } catch (ClassNotFoundException e) { + throw new UtilException(e, "[{}] not found!", className); + } + } + + @Override + public boolean isCalledBy(Class clazz) { + final StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace(); + for (final StackTraceElement element : stackTrace) { + if (element.getClassName().equals(clazz.getName())) { + return true; + } + } + return false; + } +} diff --git a/src/main/java/cn/hutool/core/lang/caller/package-info.java b/src/main/java/cn/hutool/core/lang/caller/package-info.java new file mode 100644 index 0000000..5380baf --- /dev/null +++ b/src/main/java/cn/hutool/core/lang/caller/package-info.java @@ -0,0 +1,7 @@ +/** + * 调用者接口及实现。可以通过此类的方法获取调用者、多级调用者以及判断是否被调用 + * + * @author looly + * + */ +package cn.hutool.core.lang.caller; \ No newline at end of file diff --git a/src/main/java/cn/hutool/core/lang/copier/Copier.java b/src/main/java/cn/hutool/core/lang/copier/Copier.java new file mode 100644 index 0000000..84c2f4b --- /dev/null +++ b/src/main/java/cn/hutool/core/lang/copier/Copier.java @@ -0,0 +1,16 @@ +package cn.hutool.core.lang.copier; + +/** + * 拷贝接口 + * @author Looly + * + * @param 拷贝目标类型 + */ +@FunctionalInterface +public interface Copier { + /** + * 执行拷贝 + * @return 拷贝的目标 + */ + T copy(); +} diff --git a/src/main/java/cn/hutool/core/lang/copier/SrcToDestCopier.java b/src/main/java/cn/hutool/core/lang/copier/SrcToDestCopier.java new file mode 100644 index 0000000..8efd688 --- /dev/null +++ b/src/main/java/cn/hutool/core/lang/copier/SrcToDestCopier.java @@ -0,0 +1,86 @@ +package cn.hutool.core.lang.copier; + +import java.io.Serializable; + +import cn.hutool.core.lang.Filter; + +/** + * 复制器抽象类
+ * 抽象复制器抽象了一个对象复制到另一个对象,通过实现{@link #copy()}方法实现复制逻辑。
+ * + * @author Looly + * + * @param 拷贝的对象 + * @param 本类的类型。用于set方法返回本对象,方便流式编程 + * @since 3.0.9 + */ +public abstract class SrcToDestCopier> implements Copier, Serializable{ + private static final long serialVersionUID = 1L; + + /** 源 */ + protected T src; + /** 目标 */ + protected T dest; + /** 拷贝过滤器,可以过滤掉不需要拷贝的源 */ + protected Filter copyFilter; + + //-------------------------------------------------------------------------------------------------------- Getters and Setters start + /** + * 获取源 + * @return 源 + */ + public T getSrc() { + return src; + } + /** + * 设置源 + * + * @param src 源 + * @return this + */ + @SuppressWarnings("unchecked") + public C setSrc(T src) { + this.src = src; + return (C)this; + } + + /** + * 获得目标 + * + * @return 目标 + */ + public T getDest() { + return dest; + } + /** + * 设置目标 + * + * @param dest 目标 + * @return this + */ + @SuppressWarnings("unchecked") + public C setDest(T dest) { + this.dest = dest; + return (C)this; + } + + /** + * 获得过滤器 + * @return 过滤器 + */ + public Filter getCopyFilter() { + return copyFilter; + } + /** + * 设置过滤器 + * + * @param copyFilter 过滤器 + * @return this + */ + @SuppressWarnings("unchecked") + public C setCopyFilter(Filter copyFilter) { + this.copyFilter = copyFilter; + return (C)this; + } + //-------------------------------------------------------------------------------------------------------- Getters and Setters end +} diff --git a/src/main/java/cn/hutool/core/lang/copier/package-info.java b/src/main/java/cn/hutool/core/lang/copier/package-info.java new file mode 100644 index 0000000..75f0bab --- /dev/null +++ b/src/main/java/cn/hutool/core/lang/copier/package-info.java @@ -0,0 +1,7 @@ +/** + * 拷贝抽象实现,通过抽象拷贝,可以实现文件、流、Buffer之间的拷贝实现 + * + * @author looly + * + */ +package cn.hutool.core.lang.copier; \ No newline at end of file diff --git a/src/main/java/cn/hutool/core/lang/func/Consumer3.java b/src/main/java/cn/hutool/core/lang/func/Consumer3.java new file mode 100644 index 0000000..b40a2c3 --- /dev/null +++ b/src/main/java/cn/hutool/core/lang/func/Consumer3.java @@ -0,0 +1,23 @@ +package cn.hutool.core.lang.func; + +/** + * 3参数Consumer + * + * @param 参数一类型 + * @param 参数二类型 + * @param 参数三类型 + * @author TomXin + * @since 5.7.22 + */ +@FunctionalInterface +public interface Consumer3 { + + /** + * 接收参数方法 + * + * @param p1 参数一 + * @param p2 参数二 + * @param p3 参数三 + */ + void accept(P1 p1, P2 p2, P3 p3); +} diff --git a/src/main/java/cn/hutool/core/lang/func/Func.java b/src/main/java/cn/hutool/core/lang/func/Func.java new file mode 100644 index 0000000..8f164dc --- /dev/null +++ b/src/main/java/cn/hutool/core/lang/func/Func.java @@ -0,0 +1,43 @@ +package cn.hutool.core.lang.func; + +import java.io.Serializable; + +/** + * 函数对象
+ * 接口灵感来自于ActFramework
+ * 一个函数接口代表一个一个函数,用于包装一个函数为对象
+ * 在JDK8之前,Java的函数并不能作为参数传递,也不能作为返回值存在,此接口用于将一个函数包装成为一个对象,从而传递对象 + * + * @author Looly + * + * @param

参数类型 + * @param 返回值类型 + * @since 3.1.0 + */ +@FunctionalInterface +public interface Func extends Serializable { + /** + * 执行函数 + * + * @param parameters 参数列表 + * @return 函数执行结果 + * @throws Exception 自定义异常 + */ + @SuppressWarnings("unchecked") + R call(P... parameters) throws Exception; + + /** + * 执行函数,异常包装为RuntimeException + * + * @param parameters 参数列表 + * @return 函数执行结果 + */ + @SuppressWarnings("unchecked") + default R callWithRuntimeException(P... parameters){ + try { + return call(parameters); + } catch (Exception e) { + throw new RuntimeException(e); + } + } +} diff --git a/src/main/java/cn/hutool/core/lang/func/Func0.java b/src/main/java/cn/hutool/core/lang/func/Func0.java new file mode 100644 index 0000000..ae7216e --- /dev/null +++ b/src/main/java/cn/hutool/core/lang/func/Func0.java @@ -0,0 +1,39 @@ +package cn.hutool.core.lang.func; + +import java.io.Serializable; + +/** + * 无参数的函数对象
+ * 接口灵感来自于ActFramework
+ * 一个函数接口代表一个一个函数,用于包装一个函数为对象
+ * 在JDK8之前,Java的函数并不能作为参数传递,也不能作为返回值存在,此接口用于将一个函数包装成为一个对象,从而传递对象 + * + * @author Looly + * + * @param 返回值类型 + * @since 4.5.2 + */ +@FunctionalInterface +public interface Func0 extends Serializable { + /** + * 执行函数 + * + * @return 函数执行结果 + * @throws Exception 自定义异常 + */ + R call() throws Exception; + + /** + * 执行函数,异常包装为RuntimeException + * + * @return 函数执行结果 + * @since 5.3.6 + */ + default R callWithRuntimeException(){ + try { + return call(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } +} diff --git a/src/main/java/cn/hutool/core/lang/func/Func1.java b/src/main/java/cn/hutool/core/lang/func/Func1.java new file mode 100644 index 0000000..303af09 --- /dev/null +++ b/src/main/java/cn/hutool/core/lang/func/Func1.java @@ -0,0 +1,43 @@ +package cn.hutool.core.lang.func; + +import java.io.Serializable; + +/** + * 只有一个参数的函数对象
+ * 接口灵感来自于ActFramework
+ * 一个函数接口代表一个一个函数,用于包装一个函数为对象
+ * 在JDK8之前,Java的函数并不能作为参数传递,也不能作为返回值存在,此接口用于将一个函数包装成为一个对象,从而传递对象 + * + * @author Looly + * + * @param

参数类型 + * @param 返回值类型 + * @since 4.2.2 + */ +@FunctionalInterface +public interface Func1 extends Serializable { + + /** + * 执行函数 + * + * @param parameter 参数 + * @return 函数执行结果 + * @throws Exception 自定义异常 + */ + R call(P parameter) throws Exception; + + /** + * 执行函数,异常包装为RuntimeException + * + * @param parameter 参数 + * @return 函数执行结果 + * @since 5.3.6 + */ + default R callWithRuntimeException(P parameter){ + try { + return call(parameter); + } catch (Exception e) { + throw new RuntimeException(e); + } + } +} diff --git a/src/main/java/cn/hutool/core/lang/func/LambdaUtil.java b/src/main/java/cn/hutool/core/lang/func/LambdaUtil.java new file mode 100644 index 0000000..da4bb61 --- /dev/null +++ b/src/main/java/cn/hutool/core/lang/func/LambdaUtil.java @@ -0,0 +1,208 @@ +package cn.hutool.core.lang.func; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.map.WeakConcurrentMap; +import cn.hutool.core.util.ClassUtil; +import cn.hutool.core.util.ReflectUtil; +import cn.hutool.core.util.StrUtil; + +import java.io.Serializable; +import java.lang.invoke.MethodHandleInfo; +import java.lang.invoke.SerializedLambda; + +/** + * Lambda相关工具类 + * + * @author looly, Scen + * @since 5.6.3 + */ +public class LambdaUtil { + + private static final WeakConcurrentMap cache = new WeakConcurrentMap<>(); + + /** + * 通过对象的方法或类的静态方法引用,获取lambda实现类 + * 传入lambda无参数但含有返回值的情况能够匹配到此方法: + *

    + *
  • 引用特定对象的实例方法:
    {@code
    +	 * MyTeacher myTeacher = new MyTeacher();
    +	 * Class supplierClass = LambdaUtil.getRealClass(myTeacher::getAge);
    +	 * Assert.assertEquals(MyTeacher.class, supplierClass);
    +	 * }
  • + *
  • 引用静态无参方法:
    {@code
    +	 * Class staticSupplierClass = LambdaUtil.getRealClass(MyTeacher::takeAge);
    +	 * Assert.assertEquals(MyTeacher.class, staticSupplierClass);
    +	 * }
  • + *
+ * 在以下场景无法获取到正确类型 + *
{@code
+	 * // 枚举测试,只能获取到枚举类型
+	 * Class> enumSupplierClass = LambdaUtil.getRealClass(LambdaUtil.LambdaKindEnum.REF_NONE::ordinal);
+	 * Assert.assertEquals(Enum.class, enumSupplierClass);
+	 * // 调用父类方法,只能获取到父类类型
+	 * Class> superSupplierClass = LambdaUtil.getRealClass(myTeacher::getId);
+	 * Assert.assertEquals(Entity.class, superSupplierClass);
+	 * // 引用父类静态带参方法,只能获取到父类类型
+	 * Class> staticSuperFunctionClass = LambdaUtil.getRealClass(MyTeacher::takeId);
+	 * Assert.assertEquals(Entity.class, staticSuperFunctionClass);
+	 * }
+ * + * @param func lambda + * @param 类型 + * @return lambda实现类 + * @throws IllegalArgumentException 如果是不支持的方法引用,抛出该异常,见{@link LambdaUtil#checkLambdaTypeCanGetClass} + * @since 5.8.0 + * @author VampireAchao + */ + public static Class getRealClass(Func0 func) { + final SerializedLambda lambda = resolve(func); + checkLambdaTypeCanGetClass(lambda.getImplMethodKind()); + return ClassUtil.loadClass(lambda.getImplClass()); + } + + /** + * 解析lambda表达式,加了缓存。 + * 该缓存可能会在任意不定的时间被清除 + * + * @param Lambda类型 + * @param func 需要解析的 lambda 对象(无参方法) + * @return 返回解析后的结果 + */ + public static SerializedLambda resolve(Func1 func) { + return _resolve(func); + } + + /** + * 解析lambda表达式,加了缓存。 + * 该缓存可能会在任意不定的时间被清除 + * + * @param Lambda返回类型 + * @param func 需要解析的 lambda 对象(无参方法) + * @return 返回解析后的结果 + * @since 5.7.23 + */ + public static SerializedLambda resolve(Func0 func) { + return _resolve(func); + } + + /** + * 获取lambda表达式函数(方法)名称 + * + * @param

Lambda参数类型 + * @param func 函数(无参方法) + * @return 函数名称 + */ + public static

String getMethodName(Func1 func) { + return resolve(func).getImplMethodName(); + } + + /** + * 获取lambda表达式函数(方法)名称 + * + * @param Lambda返回类型 + * @param func 函数(无参方法) + * @return 函数名称 + * @since 5.7.23 + */ + public static String getMethodName(Func0 func) { + return resolve(func).getImplMethodName(); + } + + /** + * 通过对象的方法或类的静态方法引用,然后根据{@link SerializedLambda#getInstantiatedMethodType()}获取lambda实现类
+ * 传入lambda有参数且含有返回值的情况能够匹配到此方法: + *

    + *
  • 引用特定类型的任意对象的实例方法:
    {@code
    +	 * Class functionClass = LambdaUtil.getRealClass(MyTeacher::getAge);
    +	 * Assert.assertEquals(MyTeacher.class, functionClass);
    +	 * }
  • + *
  • 引用静态带参方法:
    {@code
    +	 * Class staticFunctionClass = LambdaUtil.getRealClass(MyTeacher::takeAgeBy);
    +	 * Assert.assertEquals(MyTeacher.class, staticFunctionClass);
    +	 * }
  • + *
+ * + * @param func lambda + * @param

方法调用方类型 + * @param 返回值类型 + * @return lambda实现类 + * @throws IllegalArgumentException 如果是不支持的方法引用,抛出该异常,见{@link LambdaUtil#checkLambdaTypeCanGetClass} + * @since 5.8.0 + * @author VampireAchao + */ + public static Class

getRealClass(Func1 func) { + final SerializedLambda lambda = resolve(func); + checkLambdaTypeCanGetClass(lambda.getImplMethodKind()); + final String instantiatedMethodType = lambda.getInstantiatedMethodType(); + return ClassUtil.loadClass(StrUtil.sub(instantiatedMethodType, 2, StrUtil.indexOf(instantiatedMethodType, ';'))); + } + + /** + * 获取lambda表达式Getter或Setter函数(方法)对应的字段名称,规则如下: + *

    + *
  • getXxxx获取为xxxx,如getName得到name。
  • + *
  • setXxxx获取为xxxx,如setName得到name。
  • + *
  • isXxxx获取为xxxx,如isName得到name。
  • + *
  • 其它不满足规则的方法名抛出{@link IllegalArgumentException}
  • + *
+ * + * @param Lambda类型 + * @param func 函数(无参方法) + * @return 方法名称 + * @throws IllegalArgumentException 非Getter或Setter方法 + * @since 5.7.10 + */ + public static String getFieldName(Func1 func) throws IllegalArgumentException { + return BeanUtil.getFieldName(getMethodName(func)); + } + + /** + * 获取lambda表达式Getter或Setter函数(方法)对应的字段名称,规则如下: + *
    + *
  • getXxxx获取为xxxx,如getName得到name。
  • + *
  • setXxxx获取为xxxx,如setName得到name。
  • + *
  • isXxxx获取为xxxx,如isName得到name。
  • + *
  • 其它不满足规则的方法名抛出{@link IllegalArgumentException}
  • + *
+ * + * @param Lambda类型 + * @param func 函数(无参方法) + * @return 方法名称 + * @throws IllegalArgumentException 非Getter或Setter方法 + * @since 5.7.23 + */ + public static String getFieldName(Func0 func) throws IllegalArgumentException { + return BeanUtil.getFieldName(getMethodName(func)); + } + + //region Private methods + /** + * 检查是否为支持的类型 + * + * @param implMethodKind 支持的lambda类型 + * @throws IllegalArgumentException 如果是不支持的方法引用,抛出该异常 + */ + private static void checkLambdaTypeCanGetClass(int implMethodKind) { + if (implMethodKind != MethodHandleInfo.REF_invokeVirtual && + implMethodKind != MethodHandleInfo.REF_invokeStatic) { + throw new IllegalArgumentException("该lambda不是合适的方法引用"); + } + } + + /** + * 解析lambda表达式,加了缓存。 + * 该缓存可能会在任意不定的时间被清除。 + * + *

+ * 通过反射调用实现序列化接口函数对象的writeReplace方法,从而拿到{@link SerializedLambda}
+ * 该对象中包含了lambda表达式的所有信息。 + *

+ * + * @param func 需要解析的 lambda 对象 + * @return 返回解析后的结果 + */ + private static SerializedLambda _resolve(Serializable func) { + return cache.computeIfAbsent(func.getClass().getName(), (key) -> ReflectUtil.invoke(func, "writeReplace")); + } + //endregion +} diff --git a/src/main/java/cn/hutool/core/lang/func/Supplier1.java b/src/main/java/cn/hutool/core/lang/func/Supplier1.java new file mode 100644 index 0000000..376c91a --- /dev/null +++ b/src/main/java/cn/hutool/core/lang/func/Supplier1.java @@ -0,0 +1,32 @@ +package cn.hutool.core.lang.func; + +import java.util.function.Supplier; + +/** + * 1参数Supplier + * + * @param 目标 类型 + * @param 参数一 类型 + * @author TomXin + * @since 5.7.21 + */ +@FunctionalInterface +public interface Supplier1 { + /** + * 生成实例的方法 + * + * @param p1 参数一 + * @return 目标对象 + */ + T get(P1 p1); + + /** + * 将带有参数的Supplier转换为无参{@link Supplier} + * + * @param p1 参数1 + * @return {@link Supplier} + */ + default Supplier toSupplier(P1 p1) { + return () -> get(p1); + } +} diff --git a/src/main/java/cn/hutool/core/lang/func/Supplier2.java b/src/main/java/cn/hutool/core/lang/func/Supplier2.java new file mode 100644 index 0000000..6d5e9ce --- /dev/null +++ b/src/main/java/cn/hutool/core/lang/func/Supplier2.java @@ -0,0 +1,36 @@ +package cn.hutool.core.lang.func; + +import java.util.function.Supplier; + +/** + * 两个参数的Supplier + * + * @param 目标 类型 + * @param 参数一 类型 + * @param 参数二 类型 + * @author TomXin + * @since 5.7.21 + */ +@FunctionalInterface +public interface Supplier2 { + + /** + * 生成实例的方法 + * + * @param p1 参数一 + * @param p2 参数二 + * @return 目标对象 + */ + T get(P1 p1, P2 p2); + + /** + * 将带有参数的Supplier转换为无参{@link Supplier} + * + * @param p1 参数1 + * @param p2 参数2 + * @return {@link Supplier} + */ + default Supplier toSupplier(P1 p1, P2 p2) { + return () -> get(p1, p2); + } +} diff --git a/src/main/java/cn/hutool/core/lang/func/Supplier3.java b/src/main/java/cn/hutool/core/lang/func/Supplier3.java new file mode 100644 index 0000000..50324e5 --- /dev/null +++ b/src/main/java/cn/hutool/core/lang/func/Supplier3.java @@ -0,0 +1,39 @@ +package cn.hutool.core.lang.func; + +import java.util.function.Supplier; + +/** + * 3参数Supplier + * + * @param 目标类型 + * @param 参数一类型 + * @param 参数二类型 + * @param 参数三类型 + * @author TomXin + * @since 5.7.21 + */ +@FunctionalInterface +public interface Supplier3 { + + /** + * 生成实例的方法 + * + * @param p1 参数一 + * @param p2 参数二 + * @param p3 参数三 + * @return 目标对象 + */ + T get(P1 p1, P2 p2, P3 p3); + + /** + * 将带有参数的Supplier转换为无参{@link Supplier} + * + * @param p1 参数1 + * @param p2 参数2 + * @param p3 参数3 + * @return {@link Supplier} + */ + default Supplier toSupplier(P1 p1, P2 p2, P3 p3) { + return () -> get(p1, p2, p3); + } +} diff --git a/src/main/java/cn/hutool/core/lang/func/Supplier4.java b/src/main/java/cn/hutool/core/lang/func/Supplier4.java new file mode 100644 index 0000000..c715b86 --- /dev/null +++ b/src/main/java/cn/hutool/core/lang/func/Supplier4.java @@ -0,0 +1,42 @@ +package cn.hutool.core.lang.func; + +import java.util.function.Supplier; + +/** + * 4参数Supplier + * + * @param 目标 类型 + * @param 参数一 类型 + * @param 参数二 类型 + * @param 参数三 类型 + * @param 参数四 类型 + * @author TomXin + * @since 5.7.21 + */ +@FunctionalInterface +public interface Supplier4 { + + /** + * 生成实例的方法 + * + * @param p1 参数一 + * @param p2 参数二 + * @param p3 参数三 + * @param p4 参数四 + * @return 目标对象 + */ + T get(P1 p1, P2 p2, P3 p3, P4 p4); + + /** + * 将带有参数的Supplier转换为无参{@link Supplier} + * + * @param p1 参数1 + * @param p2 参数2 + * @param p3 参数3 + * @param p4 参数4 + * @return {@link Supplier} + */ + default Supplier toSupplier(P1 p1, P2 p2, P3 p3, P4 p4) { + return () -> get(p1, p2, p3, p4); + } +} diff --git a/src/main/java/cn/hutool/core/lang/func/Supplier5.java b/src/main/java/cn/hutool/core/lang/func/Supplier5.java new file mode 100644 index 0000000..121a2ce --- /dev/null +++ b/src/main/java/cn/hutool/core/lang/func/Supplier5.java @@ -0,0 +1,45 @@ +package cn.hutool.core.lang.func; + +import java.util.function.Supplier; + +/** + * 5参数Supplier + * + * @param 目标 类型 + * @param 参数一 类型 + * @param 参数二 类型 + * @param 参数三 类型 + * @param 参数四 类型 + * @param 参数五 类型 + * @author TomXin + * @since 5.7.21 + */ +@FunctionalInterface +public interface Supplier5 { + + /** + * 生成实例的方法 + * + * @param p1 参数一 + * @param p2 参数二 + * @param p3 参数三 + * @param p4 参数四 + * @param p5 参数五 + * @return 目标对象 + */ + T get(P1 p1, P2 p2, P3 p3, P4 p4, P5 p5); + + /** + * 将带有参数的Supplier转换为无参{@link Supplier} + * + * @param p1 参数1 + * @param p2 参数2 + * @param p3 参数3 + * @param p4 参数4 + * @param p5 参数5 + * @return {@link Supplier} + */ + default Supplier toSupplier(P1 p1, P2 p2, P3 p3, P4 p4, P5 p5) { + return () -> get(p1, p2, p3, p4, p5); + } +} diff --git a/src/main/java/cn/hutool/core/lang/func/VoidFunc.java b/src/main/java/cn/hutool/core/lang/func/VoidFunc.java new file mode 100644 index 0000000..9caa902 --- /dev/null +++ b/src/main/java/cn/hutool/core/lang/func/VoidFunc.java @@ -0,0 +1,41 @@ +package cn.hutool.core.lang.func; + +import java.io.Serializable; + +/** + * 函数对象
+ * 接口灵感来自于ActFramework
+ * 一个函数接口代表一个一个函数,用于包装一个函数为对象
+ * 在JDK8之前,Java的函数并不能作为参数传递,也不能作为返回值存在,此接口用于将一个函数包装成为一个对象,从而传递对象 + * + * @author Looly + * + * @param

参数类型 + * @since 3.1.0 + */ +@FunctionalInterface +public interface VoidFunc

extends Serializable { + + /** + * 执行函数 + * + * @param parameters 参数列表 + * @throws Exception 自定义异常 + */ + @SuppressWarnings("unchecked") + void call(P... parameters) throws Exception; + + /** + * 执行函数,异常包装为RuntimeException + * + * @param parameters 参数列表 + */ + @SuppressWarnings("unchecked") + default void callWithRuntimeException(P... parameters){ + try { + call(parameters); + } catch (Exception e) { + throw new RuntimeException(e); + } + } +} diff --git a/src/main/java/cn/hutool/core/lang/func/VoidFunc0.java b/src/main/java/cn/hutool/core/lang/func/VoidFunc0.java new file mode 100644 index 0000000..f96ad21 --- /dev/null +++ b/src/main/java/cn/hutool/core/lang/func/VoidFunc0.java @@ -0,0 +1,37 @@ +package cn.hutool.core.lang.func; + +import java.io.Serializable; + +/** + * 函数对象
+ * 接口灵感来自于ActFramework
+ * 一个函数接口代表一个一个函数,用于包装一个函数为对象
+ * 在JDK8之前,Java的函数并不能作为参数传递,也不能作为返回值存在,此接口用于将一个函数包装成为一个对象,从而传递对象 + * + * @author Looly + * + * @since 3.2.3 + */ +@FunctionalInterface +public interface VoidFunc0 extends Serializable { + + /** + * 执行函数 + * + * @throws Exception 自定义异常 + */ + void call() throws Exception; + + /** + * 执行函数,异常包装为RuntimeException + * + * @since 5.3.6 + */ + default void callWithRuntimeException(){ + try { + call(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } +} diff --git a/src/main/java/cn/hutool/core/lang/func/VoidFunc1.java b/src/main/java/cn/hutool/core/lang/func/VoidFunc1.java new file mode 100644 index 0000000..48f87bf --- /dev/null +++ b/src/main/java/cn/hutool/core/lang/func/VoidFunc1.java @@ -0,0 +1,39 @@ +package cn.hutool.core.lang.func; + +import java.io.Serializable; + +/** + * 函数对象
+ * 接口灵感来自于ActFramework
+ * 一个函数接口代表一个一个函数,用于包装一个函数为对象
+ * 在JDK8之前,Java的函数并不能作为参数传递,也不能作为返回值存在,此接口用于将一个函数包装成为一个对象,从而传递对象 + * + * @author Looly + * + * @since 3.2.3 + */ +@FunctionalInterface +public interface VoidFunc1

extends Serializable { + + /** + * 执行函数 + * + * @param parameter 参数 + * @throws Exception 自定义异常 + */ + void call(P parameter) throws Exception; + + /** + * 执行函数,异常包装为RuntimeException + * + * @param parameter 参数 + * @since 5.3.6 + */ + default void callWithRuntimeException(P parameter){ + try { + call(parameter); + } catch (Exception e) { + throw new RuntimeException(e); + } + } +} diff --git a/src/main/java/cn/hutool/core/lang/func/package-info.java b/src/main/java/cn/hutool/core/lang/func/package-info.java new file mode 100644 index 0000000..84fc234 --- /dev/null +++ b/src/main/java/cn/hutool/core/lang/func/package-info.java @@ -0,0 +1,10 @@ +/** + * 函数封装
+ * 接口灵感来自于ActFramework
+ * 一个函数接口代表一个一个函数,用于包装一个函数为对象
+ * 在JDK8之前,Java的函数并不能作为参数传递,也不能作为返回值存在,此接口用于将一个函数包装成为一个对象,从而传递对象 + * + * @author looly + * + */ +package cn.hutool.core.lang.func; \ No newline at end of file diff --git a/src/main/java/cn/hutool/core/lang/generator/Generator.java b/src/main/java/cn/hutool/core/lang/generator/Generator.java new file mode 100644 index 0000000..64a624b --- /dev/null +++ b/src/main/java/cn/hutool/core/lang/generator/Generator.java @@ -0,0 +1,18 @@ +package cn.hutool.core.lang.generator; + +/** + * 生成器泛型接口
+ * 通过实现此接口可以自定义生成对象的策略 + * + * @param 生成对象类型 + * @since 5.4.3 + */ +public interface Generator { + + /** + * 生成新的对象 + * + * @return 新的对象 + */ + T next(); +} diff --git a/src/main/java/cn/hutool/core/lang/generator/ObjectGenerator.java b/src/main/java/cn/hutool/core/lang/generator/ObjectGenerator.java new file mode 100644 index 0000000..bda200d --- /dev/null +++ b/src/main/java/cn/hutool/core/lang/generator/ObjectGenerator.java @@ -0,0 +1,28 @@ +package cn.hutool.core.lang.generator; + +import cn.hutool.core.util.ReflectUtil; + +/** + * 对象生成器,通过指定对象的Class类型,调用next方法时生成新的对象。 + * + * @param 对象类型 + * @author looly + * @since 5.4.3 + */ +public class ObjectGenerator implements Generator { + + private final Class clazz; + + /** + * 构造 + * @param clazz 对象类型 + */ + public ObjectGenerator(Class clazz) { + this.clazz = clazz; + } + + @Override + public T next() { + return ReflectUtil.newInstanceIfPossible(this.clazz); + } +} diff --git a/src/main/java/cn/hutool/core/lang/generator/package-info.java b/src/main/java/cn/hutool/core/lang/generator/package-info.java new file mode 100644 index 0000000..76ab4d9 --- /dev/null +++ b/src/main/java/cn/hutool/core/lang/generator/package-info.java @@ -0,0 +1,7 @@ +/** + * 提供生成器接口及相关封装 + * + * @author looly + * + */ +package cn.hutool.core.lang.generator; \ No newline at end of file diff --git a/src/main/java/cn/hutool/core/lang/hash/CityHash.java b/src/main/java/cn/hutool/core/lang/hash/CityHash.java new file mode 100644 index 0000000..b5558ea --- /dev/null +++ b/src/main/java/cn/hutool/core/lang/hash/CityHash.java @@ -0,0 +1,499 @@ +package cn.hutool.core.lang.hash; + +import cn.hutool.core.util.ByteUtil; + +import java.util.Arrays; + +/** + * Google发布的Hash计算算法:CityHash64 与 CityHash128。
+ * 它们分别根据字串计算 64 和 128 位的散列值。这些算法不适用于加密,但适合用在散列表等处。 + * + *

+ * 代码来自:https://github.com/rolandhe/string-tools
+ * 原始算法:https://github.com/google/cityhash + * + * @author hexiufeng + * @since 5.2.5 + */ +public class CityHash { + + // Some primes between 2^63 and 2^64 for various uses. + private static final long k0 = 0xc3a5c85c97cb3127L; + private static final long k1 = 0xb492b66fbe98f273L; + private static final long k2 = 0x9ae16a3b2f90404fL; + private static final long kMul = 0x9ddfea08eb382d69L; + + // Magic numbers for 32-bit hashing. Copied from Murmur3. + private static final int c1 = 0xcc9e2d51; + private static final int c2 = 0x1b873593; + + + /** + * 计算32位City Hash值 + * + * @param data 数据 + * @return hash值 + */ + public static int hash32(byte[] data) { + int len = data.length; + if (len <= 24) { + return len <= 12 ? + (len <= 4 ? hash32Len0to4(data) : hash32Len5to12(data)) : + hash32Len13to24(data); + } + + // len > 24 + int h = len, g = c1 * len, f = g; + int a0 = rotate32(fetch32(data, len - 4) * c1, 17) * c2; + int a1 = rotate32(fetch32(data, len - 8) * c1, 17) * c2; + int a2 = rotate32(fetch32(data, len - 16) * c1, 17) * c2; + int a3 = rotate32(fetch32(data, len - 12) * c1, 17) * c2; + int a4 = rotate32(fetch32(data, len - 20) * c1, 17) * c2; + h ^= a0; + h = rotate32(h, 19); + h = h * 5 + 0xe6546b64; + h ^= a2; + h = rotate32(h, 19); + h = h * 5 + 0xe6546b64; + g ^= a1; + g = rotate32(g, 19); + g = g * 5 + 0xe6546b64; + g ^= a3; + g = rotate32(g, 19); + g = g * 5 + 0xe6546b64; + f += a4; + f = rotate32(f, 19); + f = f * 5 + 0xe6546b64; + int iters = (len - 1) / 20; + + int pos = 0; + do { + a0 = rotate32(fetch32(data, pos) * c1, 17) * c2; + a1 = fetch32(data, pos + 4); + a2 = rotate32(fetch32(data, pos + 8) * c1, 17) * c2; + a3 = rotate32(fetch32(data, pos + 12) * c1, 17) * c2; + a4 = fetch32(data, pos + 16); + h ^= a0; + h = rotate32(h, 18); + h = h * 5 + 0xe6546b64; + f += a1; + f = rotate32(f, 19); + f = f * c1; + g += a2; + g = rotate32(g, 18); + g = g * 5 + 0xe6546b64; + h ^= a3 + a1; + h = rotate32(h, 19); + h = h * 5 + 0xe6546b64; + g ^= a4; + g = Integer.reverseBytes(g) * 5; + h += a4 * 5; + h = Integer.reverseBytes(h); + f += a0; + int swapValue = f; + f = g; + g = h; + h = swapValue; + + pos += 20; + } while (--iters != 0); + + g = rotate32(g, 11) * c1; + g = rotate32(g, 17) * c1; + f = rotate32(f, 11) * c1; + f = rotate32(f, 17) * c1; + h = rotate32(h + g, 19); + h = h * 5 + 0xe6546b64; + h = rotate32(h, 17) * c1; + h = rotate32(h + f, 19); + h = h * 5 + 0xe6546b64; + h = rotate32(h, 17) * c1; + return h; + } + + /** + * 计算64位City Hash值 + * + * @param data 数据 + * @return hash值 + */ + public static long hash64(byte[] data) { + int len = data.length; + if (len <= 32) { + if (len <= 16) { + return hashLen0to16(data); + } else { + return hashLen17to32(data); + } + } else if (len <= 64) { + return hashLen33to64(data); + } + + // For strings over 64 bytes we hash the end first, and then as we + // loop we keep 56 bytes of state: v, w, x, y, and z. + long x = fetch64(data, len - 40); + long y = fetch64(data, len - 16) + fetch64(data, len - 56); + long z = hashLen16(fetch64(data, len - 48) + len, fetch64(data, len - 24)); + Number128 v = weakHashLen32WithSeeds(data, len - 64, len, z); + Number128 w = weakHashLen32WithSeeds(data, len - 32, y + k1, x); + x = x * k1 + fetch64(data, 0); + + // Decrease len to the nearest multiple of 64, and operate on 64-byte chunks. + len = (len - 1) & ~63; + int pos = 0; + do { + x = rotate64(x + y + v.getLowValue() + fetch64(data, pos + 8), 37) * k1; + y = rotate64(y + v.getHighValue() + fetch64(data, pos + 48), 42) * k1; + x ^= w.getHighValue(); + y += v.getLowValue() + fetch64(data, pos + 40); + z = rotate64(z + w.getLowValue(), 33) * k1; + v = weakHashLen32WithSeeds(data, pos, v.getHighValue() * k1, x + w.getLowValue()); + w = weakHashLen32WithSeeds(data, pos + 32, z + w.getHighValue(), y + fetch64(data, pos + 16)); + // swap z,x value + long swapValue = x; + x = z; + z = swapValue; + pos += 64; + len -= 64; + } while (len != 0); + return hashLen16(hashLen16(v.getLowValue(), w.getLowValue()) + shiftMix(y) * k1 + z, + hashLen16(v.getHighValue(), w.getHighValue()) + x); + } + + /** + * 计算64位City Hash值 + * + * @param data 数据 + * @param seed0 种子1 + * @param seed1 种子2 + * @return hash值 + */ + public static long hash64(byte[] data, long seed0, long seed1) { + return hashLen16(hash64(data) - seed0, seed1); + } + + /** + * 计算64位City Hash值,种子1使用默认的{@link #k2} + * + * @param data 数据 + * @param seed 种子2 + * @return hash值 + */ + public static long hash64(byte[] data, long seed) { + return hash64(data, k2, seed); + } + + /** + * 计算128位City Hash值 + * + * @param data 数据 + * @return hash值 + */ + public static Number128 hash128(byte[] data) { + int len = data.length; + return len >= 16 ? + hash128(data, 16, + new Number128(fetch64(data, 0), fetch64(data, 8) + k0)) : + hash128(data, 0, new Number128(k0, k1)); + } + + /** + * 计算128位City Hash值 + * + * @param data 数据 + * @param seed 种子 + * @return hash值 + */ + public static Number128 hash128(byte[] data, Number128 seed) { + return hash128(data, 0, seed); + } + + //------------------------------------------------------------------------------------------------------- Private method start + private static Number128 hash128(final byte[] byteArray, int start, final Number128 seed) { + int len = byteArray.length - start; + + if (len < 128) { + return cityMurmur(Arrays.copyOfRange(byteArray, start, byteArray.length), seed); + } + + // We expect len >= 128 to be the common case. Keep 56 bytes of state: + // v, w, x, y, and z. + Number128 v = new Number128(0L, 0L); + Number128 w = new Number128(0L, 0L); + long x = seed.getLowValue(); + long y = seed.getHighValue(); + long z = len * k1; + v.setLowValue(rotate64(y ^ k1, 49) * k1 + fetch64(byteArray, start)); + v.setHighValue(rotate64(v.getLowValue(), 42) * k1 + fetch64(byteArray, start + 8)); + w.setLowValue(rotate64(y + z, 35) * k1 + x); + w.setHighValue(rotate64(x + fetch64(byteArray, start + 88), 53) * k1); + + // This is the same inner loop as CityHash64(), manually unrolled. + int pos = start; + do { + x = rotate64(x + y + v.getLowValue() + fetch64(byteArray, pos + 8), 37) * k1; + y = rotate64(y + v.getHighValue() + fetch64(byteArray, pos + 48), 42) * k1; + x ^= w.getHighValue(); + y += v.getLowValue() + fetch64(byteArray, pos + 40); + z = rotate64(z + w.getLowValue(), 33) * k1; + v = weakHashLen32WithSeeds(byteArray, pos, v.getHighValue() * k1, x + w.getLowValue()); + w = weakHashLen32WithSeeds(byteArray, pos + 32, z + w.getHighValue(), y + fetch64(byteArray, pos + 16)); + + long swapValue = x; + x = z; + z = swapValue; + pos += 64; + x = rotate64(x + y + v.getLowValue() + fetch64(byteArray, pos + 8), 37) * k1; + y = rotate64(y + v.getHighValue() + fetch64(byteArray, pos + 48), 42) * k1; + x ^= w.getHighValue(); + y += v.getLowValue() + fetch64(byteArray, pos + 40); + z = rotate64(z + w.getLowValue(), 33) * k1; + v = weakHashLen32WithSeeds(byteArray, pos, v.getHighValue() * k1, x + w.getLowValue()); + w = weakHashLen32WithSeeds(byteArray, pos + 32, z + w.getHighValue(), y + fetch64(byteArray, pos + 16)); + swapValue = x; + x = z; + z = swapValue; + pos += 64; + len -= 128; + } while (len >= 128); + x += rotate64(v.getLowValue() + z, 49) * k0; + y = y * k0 + rotate64(w.getHighValue(), 37); + z = z * k0 + rotate64(w.getLowValue(), 27); + w.setLowValue(w.getLowValue() * 9); + v.setLowValue(v.getLowValue() * k0); + + // If 0 < len < 128, hash up to 4 chunks of 32 bytes each from the end of s. + for (int tail_done = 0; tail_done < len; ) { + tail_done += 32; + y = rotate64(x + y, 42) * k0 + v.getHighValue(); + w.setLowValue(w.getLowValue() + fetch64(byteArray, pos + len - tail_done + 16)); + x = x * k0 + w.getLowValue(); + z += w.getHighValue() + fetch64(byteArray, pos + len - tail_done); + w.setHighValue(w.getHighValue() + v.getLowValue()); + v = weakHashLen32WithSeeds(byteArray, pos + len - tail_done, v.getLowValue() + z, v.getHighValue()); + v.setLowValue(v.getLowValue() * k0); + } + // At this point our 56 bytes of state should contain more than + // enough information for a strong 128-bit hash. We use two + // different 56-byte-to-8-byte hashes to get a 16-byte final result. + x = hashLen16(x, v.getLowValue()); + y = hashLen16(y + z, w.getLowValue()); + return new Number128(hashLen16(x + v.getHighValue(), w.getHighValue()) + y, + hashLen16(x + w.getHighValue(), y + v.getHighValue())); + + } + + private static int hash32Len0to4(final byte[] byteArray) { + int b = 0; + int c = 9; + int len = byteArray.length; + for (int v : byteArray) { + b = b * c1 + v; + c ^= b; + } + return fmix(mur(b, mur(len, c))); + } + + private static int hash32Len5to12(final byte[] byteArray) { + int len = byteArray.length; + int a = len, b = len * 5, c = 9, d = b; + a += fetch32(byteArray, 0); + b += fetch32(byteArray, len - 4); + c += fetch32(byteArray, ((len >>> 1) & 4)); + return fmix(mur(c, mur(b, mur(a, d)))); + } + + private static int hash32Len13to24(byte[] byteArray) { + int len = byteArray.length; + int a = fetch32(byteArray, (len >>> 1) - 4); + int b = fetch32(byteArray, 4); + int c = fetch32(byteArray, len - 8); + int d = fetch32(byteArray, (len >>> 1)); + int e = fetch32(byteArray, 0); + int f = fetch32(byteArray, len - 4); + @SuppressWarnings("UnnecessaryLocalVariable") + int h = len; + + return fmix(mur(f, mur(e, mur(d, mur(c, mur(b, mur(a, h))))))); + } + + private static long hashLen0to16(byte[] byteArray) { + int len = byteArray.length; + if (len >= 8) { + long mul = k2 + len * 2L; + long a = fetch64(byteArray, 0) + k2; + long b = fetch64(byteArray, len - 8); + long c = rotate64(b, 37) * mul + a; + long d = (rotate64(a, 25) + b) * mul; + return hashLen16(c, d, mul); + } + if (len >= 4) { + long mul = k2 + len * 2; + long a = fetch32(byteArray, 0) & 0xffffffffL; + return hashLen16(len + (a << 3), fetch32(byteArray, len - 4) & 0xffffffffL, mul); + } + if (len > 0) { + int a = byteArray[0] & 0xff; + int b = byteArray[len >>> 1] & 0xff; + int c = byteArray[len - 1] & 0xff; + int y = a + (b << 8); + int z = len + (c << 2); + return shiftMix(y * k2 ^ z * k0) * k2; + } + return k2; + } + + // This probably works well for 16-byte strings as well, but it may be overkill in that case. + private static long hashLen17to32(byte[] byteArray) { + int len = byteArray.length; + long mul = k2 + len * 2L; + long a = fetch64(byteArray, 0) * k1; + long b = fetch64(byteArray, 8); + long c = fetch64(byteArray, len - 8) * mul; + long d = fetch64(byteArray, len - 16) * k2; + return hashLen16(rotate64(a + b, 43) + rotate64(c, 30) + d, + a + rotate64(b + k2, 18) + c, mul); + } + + private static long hashLen33to64(byte[] byteArray) { + int len = byteArray.length; + long mul = k2 + len * 2L; + long a = fetch64(byteArray, 0) * k2; + long b = fetch64(byteArray, 8); + long c = fetch64(byteArray, len - 24); + long d = fetch64(byteArray, len - 32); + long e = fetch64(byteArray, 16) * k2; + long f = fetch64(byteArray, 24) * 9; + long g = fetch64(byteArray, len - 8); + long h = fetch64(byteArray, len - 16) * mul; + long u = rotate64(a + g, 43) + (rotate64(b, 30) + c) * 9; + long v = ((a + g) ^ d) + f + 1; + long w = Long.reverseBytes((u + v) * mul) + h; + long x = rotate64(e + f, 42) + c; + long y = (Long.reverseBytes((v + w) * mul) + g) * mul; + long z = e + f + c; + a = Long.reverseBytes((x + z) * mul + y) + b; + b = shiftMix((z + a) * mul + d + h) * mul; + return b + x; + } + + private static long fetch64(byte[] byteArray, int start) { + return ByteUtil.bytesToLong(byteArray, start, ByteUtil.CPU_ENDIAN); + } + + private static int fetch32(byte[] byteArray, final int start) { + return ByteUtil.bytesToInt(byteArray, start, ByteUtil.CPU_ENDIAN); + } + + private static long rotate64(long val, int shift) { + // Avoid shifting by 64: doing so yields an undefined result. + return shift == 0 ? val : ((val >>> shift) | (val << (64 - shift))); + } + + private static int rotate32(int val, int shift) { + // Avoid shifting by 32: doing so yields an undefined result. + return shift == 0 ? val : ((val >>> shift) | (val << (32 - shift))); + } + + private static long hashLen16(long u, long v, long mul) { + // Murmur-inspired hashing. + long a = (u ^ v) * mul; + a ^= (a >>> 47); + long b = (v ^ a) * mul; + b ^= (b >>> 47); + b *= mul; + return b; + } + + private static long hashLen16(long u, long v) { + return hash128to64(new Number128(u, v)); + } + + private static long hash128to64(final Number128 number128) { + // Murmur-inspired hashing. + long a = (number128.getLowValue() ^ number128.getHighValue()) * kMul; + a ^= (a >>> 47); + long b = (number128.getHighValue() ^ a) * kMul; + b ^= (b >>> 47); + b *= kMul; + return b; + } + + private static long shiftMix(long val) { + return val ^ (val >>> 47); + } + + private static int fmix(int h) { + h ^= h >>> 16; + h *= 0x85ebca6b; + h ^= h >>> 13; + h *= 0xc2b2ae35; + h ^= h >>> 16; + return h; + } + + private static int mur(int a, int h) { + // Helper from Murmur3 for combining two 32-bit values. + a *= c1; + a = rotate32(a, 17); + a *= c2; + h ^= a; + h = rotate32(h, 19); + return h * 5 + 0xe6546b64; + } + + private static Number128 weakHashLen32WithSeeds( + long w, long x, long y, long z, long a, long b) { + a += w; + b = rotate64(b + a + z, 21); + long c = a; + a += x; + a += y; + b += rotate64(a, 44); + return new Number128(a + z, b + c); + } + + // Return a 16-byte hash for s[0] ... s[31], a, and b. Quick and dirty. + private static Number128 weakHashLen32WithSeeds( + byte[] byteArray, int start, long a, long b) { + return weakHashLen32WithSeeds(fetch64(byteArray, start), + fetch64(byteArray, start + 8), + fetch64(byteArray, start + 16), + fetch64(byteArray, start + 24), + a, + b); + } + + private static Number128 cityMurmur(final byte[] byteArray, Number128 seed) { + int len = byteArray.length; + long a = seed.getLowValue(); + long b = seed.getHighValue(); + long c; + long d; + int l = len - 16; + if (l <= 0) { // len <= 16 + a = shiftMix(a * k1) * k1; + c = b * k1 + hashLen0to16(byteArray); + d = shiftMix(a + (len >= 8 ? fetch64(byteArray, 0) : c)); + } else { // len > 16 + c = hashLen16(fetch64(byteArray, len - 8) + k1, a); + d = hashLen16(b + len, c + fetch64(byteArray, len - 16)); + a += d; + int pos = 0; + do { + a ^= shiftMix(fetch64(byteArray, pos) * k1) * k1; + a *= k1; + b ^= a; + c ^= shiftMix(fetch64(byteArray, pos + 8) * k1) * k1; + c *= k1; + d ^= c; + pos += 16; + l -= 16; + } while (l > 0); + } + a = hashLen16(a, c); + b = hashLen16(d, b); + return new Number128(a ^ b, hashLen16(b, a)); + } + //------------------------------------------------------------------------------------------------------- Private method end +} diff --git a/src/main/java/cn/hutool/core/lang/hash/Hash.java b/src/main/java/cn/hutool/core/lang/hash/Hash.java new file mode 100644 index 0000000..8ad0a9e --- /dev/null +++ b/src/main/java/cn/hutool/core/lang/hash/Hash.java @@ -0,0 +1,19 @@ +package cn.hutool.core.lang.hash; + +/** + * Hash计算接口 + * + * @param 被计算hash的对象类型 + * @author looly + * @since 5.7.15 + */ +@FunctionalInterface +public interface Hash { + /** + * 计算Hash值 + * + * @param t 对象 + * @return hash + */ + Number hash(T t); +} diff --git a/src/main/java/cn/hutool/core/lang/hash/Hash128.java b/src/main/java/cn/hutool/core/lang/hash/Hash128.java new file mode 100644 index 0000000..b16b4c9 --- /dev/null +++ b/src/main/java/cn/hutool/core/lang/hash/Hash128.java @@ -0,0 +1,25 @@ +package cn.hutool.core.lang.hash; + +/** + * Hash计算接口 + * + * @param 被计算hash的对象类型 + * @author looly + * @since 5.2.5 + */ +@FunctionalInterface +public interface Hash128 extends Hash{ + + /** + * 计算Hash值 + * + * @param t 对象 + * @return hash + */ + Number128 hash128(T t); + + @Override + default Number hash(T t){ + return hash128(t); + } +} diff --git a/src/main/java/cn/hutool/core/lang/hash/Hash32.java b/src/main/java/cn/hutool/core/lang/hash/Hash32.java new file mode 100644 index 0000000..7a90166 --- /dev/null +++ b/src/main/java/cn/hutool/core/lang/hash/Hash32.java @@ -0,0 +1,24 @@ +package cn.hutool.core.lang.hash; + +/** + * Hash计算接口 + * + * @param 被计算hash的对象类型 + * @author looly + * @since 5.2.5 + */ +@FunctionalInterface +public interface Hash32 extends Hash{ + /** + * 计算Hash值 + * + * @param t 对象 + * @return hash + */ + int hash32(T t); + + @Override + default Number hash(T t){ + return hash32(t); + } +} diff --git a/src/main/java/cn/hutool/core/lang/hash/Hash64.java b/src/main/java/cn/hutool/core/lang/hash/Hash64.java new file mode 100644 index 0000000..61a50e4 --- /dev/null +++ b/src/main/java/cn/hutool/core/lang/hash/Hash64.java @@ -0,0 +1,24 @@ +package cn.hutool.core.lang.hash; + +/** + * Hash计算接口 + * + * @param 被计算hash的对象类型 + * @author looly + * @since 5.2.5 + */ +@FunctionalInterface +public interface Hash64 extends Hash{ + /** + * 计算Hash值 + * + * @param t 对象 + * @return hash + */ + long hash64(T t); + + @Override + default Number hash(T t){ + return hash64(t); + } +} diff --git a/src/main/java/cn/hutool/core/lang/hash/KetamaHash.java b/src/main/java/cn/hutool/core/lang/hash/KetamaHash.java new file mode 100644 index 0000000..bc2f3cb --- /dev/null +++ b/src/main/java/cn/hutool/core/lang/hash/KetamaHash.java @@ -0,0 +1,51 @@ +package cn.hutool.core.lang.hash; + +import cn.hutool.core.exceptions.UtilException; +import cn.hutool.core.util.StrUtil; + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +/** + * Ketama算法,用于在一致性Hash中快速定位服务器位置 + * + * @author looly + * @since 5.7.20 + */ +public class KetamaHash implements Hash64, Hash32 { + + @Override + public long hash64(String key) { + byte[] bKey = md5(key); + return ((long) (bKey[3] & 0xFF) << 24) + | ((long) (bKey[2] & 0xFF) << 16) + | ((long) (bKey[1] & 0xFF) << 8) + | (bKey[0] & 0xFF); + } + + @Override + public int hash32(String key) { + return (int) (hash64(key) & 0xffffffffL); + } + + @Override + public Number hash(String key) { + return hash64(key); + } + + /** + * 计算MD5值,使用UTF-8编码 + * + * @param key 被计算的键 + * @return MD5值 + */ + private static byte[] md5(String key) { + final MessageDigest md5; + try { + md5 = MessageDigest.getInstance("MD5"); + } catch (NoSuchAlgorithmException e) { + throw new UtilException("MD5 algorithm not suooport!", e); + } + return md5.digest(StrUtil.utf8Bytes(key)); + } +} diff --git a/src/main/java/cn/hutool/core/lang/hash/MetroHash.java b/src/main/java/cn/hutool/core/lang/hash/MetroHash.java new file mode 100644 index 0000000..bbf40c8 --- /dev/null +++ b/src/main/java/cn/hutool/core/lang/hash/MetroHash.java @@ -0,0 +1,217 @@ +package cn.hutool.core.lang.hash; + +import cn.hutool.core.util.ByteUtil; + +import java.nio.ByteOrder; +import java.util.Arrays; + +/** + * Apache 发布的MetroHash算法,是一组用于非加密用例的最先进的哈希函数。 + * 除了卓越的性能外,他们还以算法生成而著称。 + * + *

+ * 官方实现:https://github.com/jandrewrogers/MetroHash + * 官方文档:http://www.jandrewrogers.com/2015/05/27/metrohash/ + * Go语言实现:https://github.com/linvon/cuckoo-filter/blob/main/vendor/github.com/dgryski/go-metro/ + * @author li + */ +public class MetroHash { + + /** + * hash64 种子加盐 + */ + private final static long k0_64 = 0xD6D018F5; + private final static long k1_64 = 0xA2AA033B; + private final static long k2_64 = 0x62992FC1; + private final static long k3_64 = 0x30BC5B29; + + /** + * hash128 种子加盐 + */ + private final static long k0_128 = 0xC83A91E1; + private final static long k1_128 = 0x8648DBDB; + private final static long k2_128 = 0x7BDEC03B; + private final static long k3_128 = 0x2F5870A5; + + public static long hash64(byte[] data) { + return hash64(data, 1337); + } + + public static Number128 hash128(byte[] data) { + return hash128(data, 1337); + } + + public static long hash64(byte[] data, long seed) { + byte[] buffer = data; + long hash = (seed + k2_64) * k0_64; + + long v0, v1, v2, v3; + v0 = hash; + v1 = hash; + v2 = hash; + v3 = hash; + + if (buffer.length >= 32) { + + while (buffer.length >= 32) { + v0 += littleEndian64(buffer, 0) * k0_64; + v0 = rotateLeft64(v0, -29) + v2; + v1 += littleEndian64(buffer, 8) * k1_64; + v1 = rotateLeft64(v1, -29) + v3; + v2 += littleEndian64(buffer, 24) * k2_64; + v2 = rotateLeft64(v2, -29) + v0; + v3 += littleEndian64(buffer, 32) * k3_64; + v3 = rotateLeft64(v3, -29) + v1; + buffer = Arrays.copyOfRange(buffer, 32, buffer.length); + } + + v2 ^= rotateLeft64(((v0 + v3) * k0_64) + v1, -37) * k1_64; + v3 ^= rotateLeft64(((v1 + v2) * k1_64) + v0, -37) * k0_64; + v0 ^= rotateLeft64(((v0 + v2) * k0_64) + v3, -37) * k1_64; + v1 ^= rotateLeft64(((v1 + v3) * k1_64) + v2, -37) * k0_64; + hash += v0 ^ v1; + } + + if (buffer.length >= 16) { + v0 = hash + littleEndian64(buffer, 0) * k2_64; + v0 = rotateLeft64(v0, -29) * k3_64; + v1 = hash + littleEndian64(buffer, 8) * k2_64; + v1 = rotateLeft64(v1, -29) * k3_64; + v0 ^= rotateLeft64(v0 * k0_64, -21) + v1; + v1 ^= rotateLeft64(v1 * k3_64, -21) + v0; + hash += v1; + buffer = Arrays.copyOfRange(buffer, 16, buffer.length); + } + + if (buffer.length >= 8) { + hash += littleEndian64(buffer, 0) * k3_64; + buffer = Arrays.copyOfRange(buffer, 8, buffer.length); + hash ^= rotateLeft64(hash, -55) * k1_64; + } + + if (buffer.length >= 4) { + hash += (long) littleEndian32(Arrays.copyOfRange(buffer, 0, 4)) * k3_64; + hash ^= rotateLeft64(hash, -26) * k1_64; + buffer = Arrays.copyOfRange(buffer, 4, buffer.length); + } + + if (buffer.length >= 2) { + hash += (long) littleEndian16(Arrays.copyOfRange(buffer, 0, 2)) * k3_64; + buffer = Arrays.copyOfRange(buffer, 2, buffer.length); + hash ^= rotateLeft64(hash, -48) * k1_64; + } + + if (buffer.length >= 1) { + hash += (long) buffer[0] * k3_64; + hash ^= rotateLeft64(hash, -38) * k1_64; + } + + hash ^= rotateLeft64(hash, -28); + hash *= k0_64; + hash ^= rotateLeft64(hash, -29); + + return hash; + } + + public static Number128 hash128(byte[] data, long seed) { + byte[] buffer = data; + + long v0, v1, v2, v3; + + v0 = (seed - k0_128) * k3_128; + v1 = (seed + k1_128) * k2_128; + + if (buffer.length >= 32) { + v2 = (seed + k0_128) * k2_128; + v3 = (seed - k1_128) * k3_128; + + while (buffer.length >= 32) { + v0 += littleEndian64(buffer, 0) * k0_128; + buffer = Arrays.copyOfRange(buffer, 8, buffer.length); + v0 = rotateRight(v0, 29) + v2; + v1 += littleEndian64(buffer, 0) * k1_128; + buffer = Arrays.copyOfRange(buffer, 8, buffer.length); + v1 = rotateRight(v1, 29) + v3; + v2 += littleEndian64(buffer, 0) * k2_128; + buffer = Arrays.copyOfRange(buffer, 8, buffer.length); + v2 = rotateRight(v2, 29) + v0; + v3 = littleEndian64(buffer, 0) * k3_128; + buffer = Arrays.copyOfRange(buffer, 8, buffer.length); + v3 = rotateRight(v3, 29) + v1; + } + + v2 ^= rotateRight(((v0 + v3) * k0_128) + v1, 21) * k1_128; + v3 ^= rotateRight(((v1 + v2) * k1_128) + v0, 21) * k0_128; + v0 ^= rotateRight(((v0 + v2) * k0_128) + v3, 21) * k1_128; + v1 ^= rotateRight(((v1 + v3) * k1_128) + v2, 21) * k0_128; + } + + if (buffer.length >= 16) { + v0 += littleEndian64(buffer, 0) * k2_128; + buffer = Arrays.copyOfRange(buffer, 8, buffer.length); + v0 = rotateRight(v0, 33) * k3_128; + v1 += littleEndian64(buffer, 0) * k2_128; + buffer = Arrays.copyOfRange(buffer, 8, buffer.length); + v1 = rotateRight(v1, 33) * k3_128; + v0 ^= rotateRight((v0 * k2_128) + v1, 45) + k1_128; + v1 ^= rotateRight((v1 * k3_128) + v0, 45) + k0_128; + } + + if (buffer.length >= 8) { + v0 += littleEndian64(buffer, 0) * k2_128; + buffer = Arrays.copyOfRange(buffer, 8, buffer.length); + v0 = rotateRight(v0, 33) * k3_128; + v0 ^= rotateRight((v0 * k2_128) + v1, 27) * k1_128; + } + + if (buffer.length >= 4) { + v1 += (long) littleEndian32(buffer) * k2_128; + buffer = Arrays.copyOfRange(buffer, 4, buffer.length); + v1 = rotateRight(v1, 33) * k3_128; + v1 ^= rotateRight((v1 * k3_128) + v0, 46) * k0_128; + } + + if (buffer.length >= 2) { + v0 += (long) littleEndian16(buffer) * k2_128; + buffer = Arrays.copyOfRange(buffer, 2, buffer.length); + v0 = rotateRight(v0, 33) * k3_128; + v0 ^= rotateRight((v0 * k2_128) * v1, 22) * k1_128; + } + + if (buffer.length >= 1) { + v1 += (long) buffer[0] * k2_128; + v1 = rotateRight(v1, 33) * k3_128; + v1 ^= rotateRight((v1 * k3_128) + v0, 58) * k0_128; + } + + v0 += rotateRight((v0 * k0_128) + v1, 13); + v1 += rotateRight((v1 * k1_128) + v0, 37); + v0 += rotateRight((v0 * k2_128) + v1, 13); + v1 += rotateRight((v1 * k3_128) + v0, 37); + + return new Number128(v0, v1); + } + + + private static long littleEndian64(byte[] b, int start) { + return ByteUtil.bytesToLong(b, start, ByteOrder.LITTLE_ENDIAN); + } + + private static int littleEndian32(byte[] b) { + return (int) b[0] | (int) b[1] << 8 | (int) b[2] << 16 | (int) b[3] << 24; + } + + private static int littleEndian16(byte[] b) { + return ByteUtil.bytesToShort(b, ByteOrder.LITTLE_ENDIAN); + } + + private static long rotateLeft64(long x, int k) { + int n = 64; + int s = k & (n - 1); + return x << s | x >> (n - s); + } + + private static long rotateRight(long val, int shift) { + return (val >> shift) | (val << (64 - shift)); + } +} diff --git a/src/main/java/cn/hutool/core/lang/hash/MurmurHash.java b/src/main/java/cn/hutool/core/lang/hash/MurmurHash.java new file mode 100644 index 0000000..0038f81 --- /dev/null +++ b/src/main/java/cn/hutool/core/lang/hash/MurmurHash.java @@ -0,0 +1,363 @@ +package cn.hutool.core.lang.hash; + +import cn.hutool.core.util.ByteUtil; +import cn.hutool.core.util.CharsetUtil; +import cn.hutool.core.util.StrUtil; + +import java.io.Serializable; +import java.nio.ByteOrder; +import java.nio.charset.Charset; + +/** + * Murmur3 32bit、64bit、128bit 哈希算法实现
+ * 此算法来自于:https://github.com/xlturing/Simhash4J/blob/master/src/main/java/bee/simhash/main/Murmur3.java + * + *

+ * 32-bit Java port of https://code.google.com/p/smhasher/source/browse/trunk/MurmurHash3.cpp#94
+ * 128-bit Java port of https://code.google.com/p/smhasher/source/browse/trunk/MurmurHash3.cpp#255 + *

+ * + * @author looly, Simhash4J + * @since 4.3.3 + */ +public class MurmurHash implements Serializable { + private static final long serialVersionUID = 1L; + + // Constants for 32 bit variant + private static final int C1_32 = 0xcc9e2d51; + private static final int C2_32 = 0x1b873593; + private static final int R1_32 = 15; + private static final int R2_32 = 13; + private static final int M_32 = 5; + private static final int N_32 = 0xe6546b64; + + // Constants for 128 bit variant + private static final long C1 = 0x87c37b91114253d5L; + private static final long C2 = 0x4cf5ad432745937fL; + private static final int R1 = 31; + private static final int R2 = 27; + private static final int R3 = 33; + private static final int M = 5; + private static final int N1 = 0x52dce729; + private static final int N2 = 0x38495ab5; + + private static final int DEFAULT_SEED = 0; + private static final Charset DEFAULT_CHARSET = CharsetUtil.CHARSET_UTF_8; + private static final ByteOrder DEFAULT_ORDER = ByteOrder.LITTLE_ENDIAN; + + /** + * Murmur3 32-bit Hash值计算 + * + * @param data 数据 + * @return Hash值 + */ + public static int hash32(CharSequence data) { + return hash32(StrUtil.bytes(data, DEFAULT_CHARSET)); + } + + /** + * Murmur3 32-bit Hash值计算 + * + * @param data 数据 + * @return Hash值 + */ + public static int hash32(byte[] data) { + return hash32(data, data.length, DEFAULT_SEED); + } + + /** + * Murmur3 32-bit Hash值计算 + * + * @param data 数据 + * @param length 长度 + * @param seed 种子,默认0 + * @return Hash值 + */ + public static int hash32(byte[] data, int length, int seed) { + return hash32(data, 0, length, seed); + } + + /** + * Murmur3 32-bit Hash值计算 + * + * @param data 数据 + * @param offset 数据开始位置 + * @param length 长度 + * @param seed 种子,默认0 + * @return Hash值 + */ + public static int hash32(byte[] data, int offset, int length, int seed) { + int hash = seed; + final int nblocks = length >> 2; + + // body + for (int i = 0; i < nblocks; i++) { + final int i4 = offset + (i << 2); + final int k = ByteUtil.bytesToInt(data, i4, DEFAULT_ORDER); + // mix functions + hash = mix32(k, hash); + } + + // tail + final int idx = offset + (nblocks << 2); + int k1 = 0; + switch (offset + length - idx) { + case 3: + k1 ^= (data[idx + 2] & 0xff) << 16; + case 2: + k1 ^= (data[idx + 1] & 0xff) << 8; + case 1: + k1 ^= (data[idx] & 0xff); + + // mix functions + k1 *= C1_32; + k1 = Integer.rotateLeft(k1, R1_32); + k1 *= C2_32; + hash ^= k1; + } + + // finalization + hash ^= length; + return fmix32(hash); + } + + /** + * Murmur3 64-bit Hash值计算 + * + * @param data 数据 + * @return Hash值 + */ + public static long hash64(CharSequence data) { + return hash64(StrUtil.bytes(data, DEFAULT_CHARSET)); + } + + /** + * Murmur3 64-bit 算法
+ * This is essentially MSB 8 bytes of Murmur3 128-bit variant. + * + * @param data 数据 + * @return Hash值 + */ + public static long hash64(byte[] data) { + return hash64(data, data.length, DEFAULT_SEED); + } + + /** + * 类Murmur3 64-bit 算法
+ * This is essentially MSB 8 bytes of Murmur3 128-bit variant. + * + * @param data 数据 + * @param length 长度 + * @param seed 种子,默认0 + * @return Hash值 + */ + public static long hash64(byte[] data, int length, int seed) { + long hash = seed; + final int nblocks = length >> 3; + + // body + for (int i = 0; i < nblocks; i++) { + final int i8 = i << 3; + long k = ByteUtil.bytesToLong(data, i8, DEFAULT_ORDER); + + // mix functions + k *= C1; + k = Long.rotateLeft(k, R1); + k *= C2; + hash ^= k; + hash = Long.rotateLeft(hash, R2) * M + N1; + } + + // tail + long k1 = 0; + int tailStart = nblocks << 3; + switch (length - tailStart) { + case 7: + k1 ^= ((long) data[tailStart + 6] & 0xff) << 48; + case 6: + k1 ^= ((long) data[tailStart + 5] & 0xff) << 40; + case 5: + k1 ^= ((long) data[tailStart + 4] & 0xff) << 32; + case 4: + k1 ^= ((long) data[tailStart + 3] & 0xff) << 24; + case 3: + k1 ^= ((long) data[tailStart + 2] & 0xff) << 16; + case 2: + k1 ^= ((long) data[tailStart + 1] & 0xff) << 8; + case 1: + k1 ^= ((long) data[tailStart] & 0xff); + k1 *= C1; + k1 = Long.rotateLeft(k1, R1); + k1 *= C2; + hash ^= k1; + } + + // finalization + hash ^= length; + hash = fmix64(hash); + + return hash; + } + + /** + * Murmur3 128-bit Hash值计算 + * + * @param data 数据 + * @return Hash值 (2 longs) + */ + public static long[] hash128(CharSequence data) { + return hash128(StrUtil.bytes(data, DEFAULT_CHARSET)); + } + + /** + * Murmur3 128-bit 算法. + * + * @param data -数据 + * @return Hash值 (2 longs) + */ + public static long[] hash128(byte[] data) { + return hash128(data, data.length, DEFAULT_SEED); + } + + /** + * Murmur3 128-bit variant. + * + * @param data 数据 + * @param length 长度 + * @param seed 种子,默认0 + * @return Hash值(2 longs) + */ + public static long[] hash128(byte[] data, int length, int seed) { + return hash128(data, 0, length, seed); + } + + /** + * Murmur3 128-bit variant. + * + * @param data 数据 + * @param offset 数据开始位置 + * @param length 长度 + * @param seed 种子,默认0 + * @return Hash值(2 longs) + */ + public static long[] hash128(byte[] data, int offset, int length, int seed) { + // 避免负数的种子 + seed &= 0xffffffffL; + + long h1 = seed; + long h2 = seed; + final int nblocks = length >> 4; + + // body + for (int i = 0; i < nblocks; i++) { + final int i16 = offset + (i << 4); + long k1 = ByteUtil.bytesToLong(data, i16, DEFAULT_ORDER); + long k2 = ByteUtil.bytesToLong(data, i16 + 8, DEFAULT_ORDER); + + // mix functions for k1 + k1 *= C1; + k1 = Long.rotateLeft(k1, R1); + k1 *= C2; + h1 ^= k1; + h1 = Long.rotateLeft(h1, R2); + h1 += h2; + h1 = h1 * M + N1; + + // mix functions for k2 + k2 *= C2; + k2 = Long.rotateLeft(k2, R3); + k2 *= C1; + h2 ^= k2; + h2 = Long.rotateLeft(h2, R1); + h2 += h1; + h2 = h2 * M + N2; + } + + // tail + long k1 = 0; + long k2 = 0; + final int tailStart = offset + (nblocks << 4); + switch (offset + length - tailStart) { + case 15: + k2 ^= (long) (data[tailStart + 14] & 0xff) << 48; + case 14: + k2 ^= (long) (data[tailStart + 13] & 0xff) << 40; + case 13: + k2 ^= (long) (data[tailStart + 12] & 0xff) << 32; + case 12: + k2 ^= (long) (data[tailStart + 11] & 0xff) << 24; + case 11: + k2 ^= (long) (data[tailStart + 10] & 0xff) << 16; + case 10: + k2 ^= (long) (data[tailStart + 9] & 0xff) << 8; + case 9: + k2 ^= data[tailStart + 8] & 0xff; + k2 *= C2; + k2 = Long.rotateLeft(k2, R3); + k2 *= C1; + h2 ^= k2; + + case 8: + k1 ^= (long) (data[tailStart + 7] & 0xff) << 56; + case 7: + k1 ^= (long) (data[tailStart + 6] & 0xff) << 48; + case 6: + k1 ^= (long) (data[tailStart + 5] & 0xff) << 40; + case 5: + k1 ^= (long) (data[tailStart + 4] & 0xff) << 32; + case 4: + k1 ^= (long) (data[tailStart + 3] & 0xff) << 24; + case 3: + k1 ^= (long) (data[tailStart + 2] & 0xff) << 16; + case 2: + k1 ^= (long) (data[tailStart + 1] & 0xff) << 8; + case 1: + k1 ^= data[tailStart] & 0xff; + k1 *= C1; + k1 = Long.rotateLeft(k1, R1); + k1 *= C2; + h1 ^= k1; + } + + // finalization + h1 ^= length; + h2 ^= length; + + h1 += h2; + h2 += h1; + + h1 = fmix64(h1); + h2 = fmix64(h2); + + h1 += h2; + h2 += h1; + + return new long[]{h1, h2}; + } + + private static int mix32(int k, int hash) { + k *= C1_32; + k = Integer.rotateLeft(k, R1_32); + k *= C2_32; + hash ^= k; + return Integer.rotateLeft(hash, R2_32) * M_32 + N_32; + } + + private static int fmix32(int hash) { + hash ^= (hash >>> 16); + hash *= 0x85ebca6b; + hash ^= (hash >>> 13); + hash *= 0xc2b2ae35; + hash ^= (hash >>> 16); + return hash; + } + + private static long fmix64(long h) { + h ^= (h >>> 33); + h *= 0xff51afd7ed558ccdL; + h ^= (h >>> 33); + h *= 0xc4ceb9fe1a85ec53L; + h ^= (h >>> 33); + return h; + } +} diff --git a/src/main/java/cn/hutool/core/lang/hash/Number128.java b/src/main/java/cn/hutool/core/lang/hash/Number128.java new file mode 100644 index 0000000..12e0a8c --- /dev/null +++ b/src/main/java/cn/hutool/core/lang/hash/Number128.java @@ -0,0 +1,90 @@ +package cn.hutool.core.lang.hash; + +/** + * 128位数字表示,分高位和低位 + * + * @author hexiufeng + * @since 5.2.5 + */ +public class Number128 extends Number { + private static final long serialVersionUID = 1L; + + private long lowValue; + private long highValue; + + /** + * 构造 + * + * @param lowValue 低位 + * @param highValue 高位 + */ + public Number128(long lowValue, long highValue) { + this.lowValue = lowValue; + this.highValue = highValue; + } + + /** + * 获取低位值 + * + * @return 地位值 + */ + public long getLowValue() { + return lowValue; + } + + /** + * 设置低位值 + * + * @param lowValue 低位值 + */ + public void setLowValue(long lowValue) { + this.lowValue = lowValue; + } + + /** + * 获取高位值 + * + * @return 高位值 + */ + public long getHighValue() { + return highValue; + } + + /** + * 设置高位值 + * + * @param hiValue 高位值 + */ + public void setHighValue(long hiValue) { + this.highValue = hiValue; + } + + /** + * 获取高低位数组,long[0]:低位,long[1]:高位 + * + * @return 高低位数组,long[0]:低位,long[1]:高位 + */ + public long[] getLongArray() { + return new long[]{lowValue, highValue}; + } + + @Override + public int intValue() { + return (int) longValue(); + } + + @Override + public long longValue() { + return this.lowValue; + } + + @Override + public float floatValue() { + return longValue(); + } + + @Override + public double doubleValue() { + return longValue(); + } +} diff --git a/src/main/java/cn/hutool/core/lang/hash/package-info.java b/src/main/java/cn/hutool/core/lang/hash/package-info.java new file mode 100644 index 0000000..08ec2a5 --- /dev/null +++ b/src/main/java/cn/hutool/core/lang/hash/package-info.java @@ -0,0 +1,7 @@ +/** + * 提供Hash算法的封装 + * + * @author looly + * + */ +package cn.hutool.core.lang.hash; \ No newline at end of file diff --git a/src/main/java/cn/hutool/core/lang/id/NanoId.java b/src/main/java/cn/hutool/core/lang/id/NanoId.java new file mode 100644 index 0000000..67045b9 --- /dev/null +++ b/src/main/java/cn/hutool/core/lang/id/NanoId.java @@ -0,0 +1,103 @@ +package cn.hutool.core.lang.id; + +import cn.hutool.core.util.RandomUtil; + +import java.security.SecureRandom; +import java.util.Random; + +/** + * NanoId,一个小型、安全、对 URL友好的唯一字符串 ID 生成器,特点: + * + *
    + *
  • 安全:它使用加密、强大的随机 API,并保证符号的正确分配
  • + *
  • 体积小:只有 258 bytes 大小(压缩后)、无依赖
  • + *
  • 紧凑:它使用比 UUID (A-Za-z0-9_~)更多的符号
  • + *
+ * + *

+ * 此实现的逻辑基于JavaScript的NanoId实现,见:https://github.com/ai/nanoid + * + * @author David Klebanoff + */ +public class NanoId { + + /** + * 默认随机数生成器,使用{@link SecureRandom}确保健壮性 + */ + private static final SecureRandom DEFAULT_NUMBER_GENERATOR = RandomUtil.getSecureRandom(); + + /** + * 默认随机字母表,使用URL安全的Base64字符 + */ + private static final char[] DEFAULT_ALPHABET = + "_-0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ".toCharArray(); + + /** + * 默认长度 + */ + public static final int DEFAULT_SIZE = 21; + + /** + * 生成伪随机的NanoId字符串,长度为默认的{@link #DEFAULT_SIZE},使用密码安全的伪随机生成器 + * + * @return 伪随机的NanoId字符串 + */ + public static String randomNanoId() { + return randomNanoId(DEFAULT_SIZE); + } + + /** + * 生成伪随机的NanoId字符串 + * + * @param size ID长度 + * @return 伪随机的NanoId字符串 + */ + public static String randomNanoId(int size) { + return randomNanoId(null, null, size); + } + + /** + * 生成伪随机的NanoId字符串 + * + * @param random 随机数生成器 + * @param alphabet 随机字母表 + * @param size ID长度 + * @return 伪随机的NanoId字符串 + */ + public static String randomNanoId(Random random, char[] alphabet, int size) { + if (random == null) { + random = DEFAULT_NUMBER_GENERATOR; + } + + if (alphabet == null) { + alphabet = DEFAULT_ALPHABET; + } + + if (alphabet.length == 0 || alphabet.length >= 256) { + throw new IllegalArgumentException("Alphabet must contain between 1 and 255 symbols."); + } + + if (size <= 0) { + throw new IllegalArgumentException("Size must be greater than zero."); + } + + final int mask = (2 << (int) Math.floor(Math.log(alphabet.length - 1) / Math.log(2))) - 1; + final int step = (int) Math.ceil(1.6 * mask * size / alphabet.length); + + final StringBuilder idBuilder = new StringBuilder(); + + while (true) { + final byte[] bytes = new byte[step]; + random.nextBytes(bytes); + for (int i = 0; i < step; i++) { + final int alphabetIndex = bytes[i] & mask; + if (alphabetIndex < alphabet.length) { + idBuilder.append(alphabet[alphabetIndex]); + if (idBuilder.length() == size) { + return idBuilder.toString(); + } + } + } + } + } +} diff --git a/src/main/java/cn/hutool/core/lang/id/package-info.java b/src/main/java/cn/hutool/core/lang/id/package-info.java new file mode 100644 index 0000000..13e9d2e --- /dev/null +++ b/src/main/java/cn/hutool/core/lang/id/package-info.java @@ -0,0 +1,7 @@ +/** + * 提供各种ID生成 + * + * @author looly + * @since 5.7.5 + */ +package cn.hutool.core.lang.id; diff --git a/src/main/java/cn/hutool/core/lang/intern/InternUtil.java b/src/main/java/cn/hutool/core/lang/intern/InternUtil.java new file mode 100644 index 0000000..7655a1a --- /dev/null +++ b/src/main/java/cn/hutool/core/lang/intern/InternUtil.java @@ -0,0 +1,40 @@ +package cn.hutool.core.lang.intern; + +/** + * 规范化对象生成工具 + * + * @author looly + * @since 5.4.3 + */ +public class InternUtil { + + /** + * 创建WeakHshMap实现的字符串规范化器 + * + * @param 规范对象的类型 + * @return {@link Interner} + */ + public static Interner createWeakInterner(){ + return new WeakInterner<>(); + } + + /** + * 创建JDK默认实现的字符串规范化器 + * + * @return {@link Interner} + * @see String#intern() + */ + public static Interner createJdkInterner(){ + return new JdkStringInterner(); + } + + /** + * 创建字符串规范化器 + * + * @param isWeak 是否创建使用WeakHashMap实现的Interner + * @return {@link Interner} + */ + public static Interner createStringInterner(boolean isWeak){ + return isWeak ? createWeakInterner() : createJdkInterner(); + } +} diff --git a/src/main/java/cn/hutool/core/lang/intern/Interner.java b/src/main/java/cn/hutool/core/lang/intern/Interner.java new file mode 100644 index 0000000..b5276a6 --- /dev/null +++ b/src/main/java/cn/hutool/core/lang/intern/Interner.java @@ -0,0 +1,21 @@ +package cn.hutool.core.lang.intern; + +/** + * 规范化表示形式封装
+ * 所谓规范化,即当两个对象equals时,规范化的对象则可以实现==
+ * 此包中的相关封装类似于 {@link String#intern()} + * + * @param 规范化的对象类型 + * @author looly + * @since 5.4.3 + */ +public interface Interner { + + /** + * 返回指定对象对应的规范化对象,sample对象可能有多个,但是这些对象如果都equals,则返回的是同一个对象 + * + * @param sample 对象 + * @return 样例对象 + */ + T intern(T sample); +} \ No newline at end of file diff --git a/src/main/java/cn/hutool/core/lang/intern/JdkStringInterner.java b/src/main/java/cn/hutool/core/lang/intern/JdkStringInterner.java new file mode 100644 index 0000000..c80717c --- /dev/null +++ b/src/main/java/cn/hutool/core/lang/intern/JdkStringInterner.java @@ -0,0 +1,17 @@ +package cn.hutool.core.lang.intern; + +/** + * JDK中默认的字符串规范化实现 + * + * @author looly + * @since 5.4.3 + */ +public class JdkStringInterner implements Interner{ + @Override + public String intern(String sample) { + if(null == sample){ + return null; + } + return sample.intern(); + } +} diff --git a/src/main/java/cn/hutool/core/lang/intern/WeakInterner.java b/src/main/java/cn/hutool/core/lang/intern/WeakInterner.java new file mode 100644 index 0000000..3358289 --- /dev/null +++ b/src/main/java/cn/hutool/core/lang/intern/WeakInterner.java @@ -0,0 +1,27 @@ +package cn.hutool.core.lang.intern; + +import cn.hutool.core.map.WeakConcurrentMap; + +import java.lang.ref.WeakReference; + +/** + * 使用WeakHashMap(线程安全)存储对象的规范化对象,注意此对象需单例使用!
+ * + * @author looly + * @since 5.4.3 + */ +public class WeakInterner implements Interner{ + + private final WeakConcurrentMap> cache = new WeakConcurrentMap<>(); + + public T intern(T sample) { + if (sample == null) { + return null; + } + T val; + do { + val = this.cache.computeIfAbsent(sample, WeakReference::new).get(); + } while (val == null); + return val; + } +} diff --git a/src/main/java/cn/hutool/core/lang/intern/package-info.java b/src/main/java/cn/hutool/core/lang/intern/package-info.java new file mode 100644 index 0000000..95e8d00 --- /dev/null +++ b/src/main/java/cn/hutool/core/lang/intern/package-info.java @@ -0,0 +1,8 @@ +/** + * 规范化表示形式封装
+ * 所谓规范化,即当两个对象equals时,规范化的对象则可以实现==
+ * 此包中的相关封装类似于 String#intern() + * + * @author looly + */ +package cn.hutool.core.lang.intern; \ No newline at end of file diff --git a/src/main/java/cn/hutool/core/lang/loader/AtomicLoader.java b/src/main/java/cn/hutool/core/lang/loader/AtomicLoader.java new file mode 100644 index 0000000..b94a407 --- /dev/null +++ b/src/main/java/cn/hutool/core/lang/loader/AtomicLoader.java @@ -0,0 +1,51 @@ +package cn.hutool.core.lang.loader; + +import java.io.Serializable; +import java.util.concurrent.atomic.AtomicReference; + +/** + * 原子引用加载器
+ * 使用{@link AtomicReference} 实懒加载,过程如下 + *

+ * 1. 检查引用中是否有加载好的对象,有则返回
+ * 2. 如果没有则初始化一个对象,并再次比较引用中是否有其它线程加载好的对象,无则加入,有则返回已有的
+ * 
+ * + * 当对象未被创建,对象的初始化操作在多线程情况下可能会被调用多次(多次创建对象),但是总是返回同一对象 + * + * @author looly + * + * @param 被加载对象类型 + */ +public abstract class AtomicLoader implements Loader, Serializable { + private static final long serialVersionUID = 1L; + + /** 被加载对象的引用 */ + private final AtomicReference reference = new AtomicReference<>(); + + /** + * 获取一个对象,第一次调用此方法时初始化对象然后返回,之后调用此方法直接返回原对象 + */ + @Override + public T get() { + T result = reference.get(); + + if (result == null) { + result = init(); + if (!reference.compareAndSet(null, result)) { + // 其它线程已经创建好此对象 + result = reference.get(); + } + } + + return result; + } + + /** + * 初始化被加载的对象
+ * 如果对象从未被加载过,调用此方法初始化加载对象,此方法只被调用一次 + * + * @return 被加载的对象 + */ + protected abstract T init(); +} diff --git a/src/main/java/cn/hutool/core/lang/loader/LazyFunLoader.java b/src/main/java/cn/hutool/core/lang/loader/LazyFunLoader.java new file mode 100644 index 0000000..0d57c5b --- /dev/null +++ b/src/main/java/cn/hutool/core/lang/loader/LazyFunLoader.java @@ -0,0 +1,78 @@ +package cn.hutool.core.lang.loader; + +import cn.hutool.core.lang.Assert; + +import java.util.function.Consumer; +import java.util.function.Supplier; + +/** + * 函数式懒加载加载器
+ * 传入用于生成对象的函数,在对象需要使用时调用生成对象,然后抛弃此生成对象的函数。
+ * 此加载器常用于对象比较庞大而不一定被使用的情况,用于减少启动时资源占用问题
+ * 继承自{@link LazyLoader},如何实现多线程安全,由LazyLoader完成。 + * + * @param 被加载对象类型 + * @author Mr.Po + * @see LazyLoader + * @since 5.6.1 + */ +public class LazyFunLoader extends LazyLoader { + private static final long serialVersionUID = 1L; + + /** + * 用于生成对象的函数 + */ + private Supplier supplier; + + /** + * 静态工厂方法,提供语义性与编码便利性 + * @param supplier 用于生成对象的函数 + * @param 对象类型 + * @return 函数式懒加载加载器对象 + * @since 5.8.0 + */ + public static LazyFunLoader on(final Supplier supplier) { + Assert.notNull(supplier, "supplier must be not null!"); + return new LazyFunLoader<>(supplier); + } + + /** + * 构造 + * + * @param supplier 用于生成对象的函数 + */ + public LazyFunLoader(Supplier supplier) { + Assert.notNull(supplier); + this.supplier = supplier; + } + + @Override + protected T init() { + T t = this.supplier.get(); + this.supplier = null; + return t; + } + + /** + * 是否已经初始化 + * + * @return 是/否 + */ + public boolean isInitialize() { + return this.supplier == null; + } + + /** + * 如果已经初始化,就执行传入函数 + * + * @param consumer 待执行函数 + */ + public void ifInitialized(Consumer consumer) { + Assert.notNull(consumer); + + // 已经初始化 + if (this.isInitialize()) { + consumer.accept(this.get()); + } + } +} diff --git a/src/main/java/cn/hutool/core/lang/loader/LazyLoader.java b/src/main/java/cn/hutool/core/lang/loader/LazyLoader.java new file mode 100644 index 0000000..5f9faa9 --- /dev/null +++ b/src/main/java/cn/hutool/core/lang/loader/LazyLoader.java @@ -0,0 +1,45 @@ +package cn.hutool.core.lang.loader; + +import java.io.Serializable; + +/** + * 懒加载加载器
+ * 在load方法被调用前,对象未被加载,直到被调用后才开始加载
+ * 此加载器常用于对象比较庞大而不一定被使用的情况,用于减少启动时资源占用问题
+ * 此加载器使用双重检查(Double-Check)方式检查对象是否被加载,避免多线程下重复加载或加载丢失问题 + * + * @author looly + * + * @param 被加载对象类型 + */ +public abstract class LazyLoader implements Loader, Serializable { + private static final long serialVersionUID = 1L; + + /** 被加载对象 */ + private volatile T object; + + /** + * 获取一个对象,第一次调用此方法时初始化对象然后返回,之后调用此方法直接返回原对象 + */ + @Override + public T get() { + T result = object; + if (result == null) { + synchronized (this) { + result = object; + if (result == null) { + object = result = init(); + } + } + } + return result; + } + + /** + * 初始化被加载的对象
+ * 如果对象从未被加载过,调用此方法初始化加载对象,此方法只被调用一次 + * + * @return 被加载的对象 + */ + protected abstract T init(); +} diff --git a/src/main/java/cn/hutool/core/lang/loader/Loader.java b/src/main/java/cn/hutool/core/lang/loader/Loader.java new file mode 100644 index 0000000..2451261 --- /dev/null +++ b/src/main/java/cn/hutool/core/lang/loader/Loader.java @@ -0,0 +1,21 @@ +package cn.hutool.core.lang.loader; + +/** + * 对象加载抽象接口
+ * 通过实现此接口自定义实现对象的加载方式,例如懒加载机制、多线程加载等 + * + * @author looly + * + * @param 对象类型 + */ +@FunctionalInterface +public interface Loader { + + /** + * 获取一个准备好的对象
+ * 通过准备逻辑准备好被加载的对象,然后返回。在准备完毕之前此方法应该被阻塞 + * + * @return 加载完毕的对象 + */ + T get(); +} diff --git a/src/main/java/cn/hutool/core/lang/loader/package-info.java b/src/main/java/cn/hutool/core/lang/loader/package-info.java new file mode 100644 index 0000000..70a0068 --- /dev/null +++ b/src/main/java/cn/hutool/core/lang/loader/package-info.java @@ -0,0 +1,7 @@ +/** + * 加载器的抽象接口和实现,包括懒加载的实现等 + * + * @author looly + * + */ +package cn.hutool.core.lang.loader; \ No newline at end of file diff --git a/src/main/java/cn/hutool/core/lang/mutable/Mutable.java b/src/main/java/cn/hutool/core/lang/mutable/Mutable.java new file mode 100644 index 0000000..e149140 --- /dev/null +++ b/src/main/java/cn/hutool/core/lang/mutable/Mutable.java @@ -0,0 +1,23 @@ +package cn.hutool.core.lang.mutable; + +/** + * 提供可变值类型接口 + * + * @param 值得类型 + * @since 3.0.1 + */ +public interface Mutable { + + /** + * 获得原始值 + * @return 原始值 + */ + T get(); + + /** + * 设置值 + * @param value 值 + */ + void set(T value); + +} \ No newline at end of file diff --git a/src/main/java/cn/hutool/core/lang/mutable/MutableBool.java b/src/main/java/cn/hutool/core/lang/mutable/MutableBool.java new file mode 100644 index 0000000..1afbfbd --- /dev/null +++ b/src/main/java/cn/hutool/core/lang/mutable/MutableBool.java @@ -0,0 +1,100 @@ +package cn.hutool.core.lang.mutable; + +import java.io.Serializable; + +/** + * 可变 {@code boolean} 类型 + * + * @see Boolean + * @since 3.0.1 + */ +public class MutableBool implements Comparable, Mutable, Serializable { + private static final long serialVersionUID = 1L; + + private boolean value; + + /** + * 构造,默认值0 + */ + public MutableBool() { + } + + /** + * 构造 + * @param value 值 + */ + public MutableBool(final boolean value) { + this.value = value; + } + + /** + * 构造 + * @param value String值 + * @throws NumberFormatException 转为Boolean错误 + */ + public MutableBool(final String value) throws NumberFormatException { + this.value = Boolean.parseBoolean(value); + } + + @Override + public Boolean get() { + return this.value; + } + + /** + * 设置值 + * @param value 值 + */ + public void set(final boolean value) { + this.value = value; + } + + @Override + public void set(final Boolean value) { + this.value = value; + } + + // ----------------------------------------------------------------------- + /** + * 相等需同时满足如下条件: + *
    + *
  1. 非空
  2. + *
  3. 类型为 MutableBool
  4. + *
  5. 值相等
  6. + *
+ * + * @param obj 比对的对象 + * @return 相同返回true,否则 {@code false} + */ + @Override + public boolean equals(final Object obj) { + if (obj instanceof MutableBool) { + return value == ((MutableBool) obj).value; + } + return false; + } + + @Override + public int hashCode() { + return value ? Boolean.TRUE.hashCode() : Boolean.FALSE.hashCode(); + } + + // ----------------------------------------------------------------------- + /** + * 比较 + * + * @param other 其它 MutableBool 对象 + * @return x==y返回0,x<y返回-1,x>y返回1 + */ + @Override + public int compareTo(final MutableBool other) { + return Boolean.compare(this.value, other.value); + } + + // ----------------------------------------------------------------------- + @Override + public String toString() { + return String.valueOf(value); + } + +} diff --git a/src/main/java/cn/hutool/core/lang/mutable/MutableByte.java b/src/main/java/cn/hutool/core/lang/mutable/MutableByte.java new file mode 100644 index 0000000..55d539f --- /dev/null +++ b/src/main/java/cn/hutool/core/lang/mutable/MutableByte.java @@ -0,0 +1,198 @@ +package cn.hutool.core.lang.mutable; + +import cn.hutool.core.util.NumberUtil; + +/** + * 可变 {@code byte} 类型 + * + * @see Byte + * @since 3.0.1 + */ +public class MutableByte extends Number implements Comparable, Mutable { + private static final long serialVersionUID = 1L; + + private byte value; + + /** + * 构造,默认值0 + */ + public MutableByte() { + } + + /** + * 构造 + * @param value 值 + */ + public MutableByte(final byte value) { + this.value = value; + } + + /** + * 构造 + * @param value 值 + */ + public MutableByte(final Number value) { + this(value.byteValue()); + } + + /** + * 构造 + * @param value String值 + * @throws NumberFormatException 转为Byte错误 + */ + public MutableByte(final String value) throws NumberFormatException { + this.value = Byte.parseByte(value); + } + + @Override + public Byte get() { + return this.value; + } + + /** + * 设置值 + * @param value 值 + */ + public void set(final byte value) { + this.value = value; + } + + @Override + public void set(final Number value) { + this.value = value.byteValue(); + } + + // ----------------------------------------------------------------------- + /** + * 值+1 + * @return this + */ + public MutableByte increment() { + value++; + return this; + } + + /** + * 值减一 + * @return this + */ + public MutableByte decrement() { + value--; + return this; + } + + // ----------------------------------------------------------------------- + /** + * 增加值 + * @param operand 被增加的值 + * @return this + */ + public MutableByte add(final byte operand) { + this.value += operand; + return this; + } + + /** + * 增加值 + * @param operand 被增加的值,非空 + * @return this + * @throws NullPointerException if the object is null + */ + public MutableByte add(final Number operand) { + this.value += operand.byteValue(); + return this; + } + + /** + * 减去值 + * + * @param operand 被减的值 + * @return this + */ + public MutableByte subtract(final byte operand) { + this.value -= operand; + return this; + } + + /** + * 减去值 + * + * @param operand 被减的值,非空 + * @return this + * @throws NullPointerException if the object is null + */ + public MutableByte subtract(final Number operand) { + this.value -= operand.byteValue(); + return this; + } + + // ----------------------------------------------------------------------- + @Override + public byte byteValue() { + return value; + } + + @Override + public int intValue() { + return value; + } + + @Override + public long longValue() { + return value; + } + + @Override + public float floatValue() { + return value; + } + + @Override + public double doubleValue() { + return value; + } + + // ----------------------------------------------------------------------- + /** + * 相等需同时满足如下条件: + *
    + *
  1. 非空
  2. + *
  3. 类型为 MutableByte
  4. + *
  5. 值相等
  6. + *
+ * + * @param obj 比对的对象 + * @return 相同返回true,否则 {@code false} + */ + @Override + public boolean equals(final Object obj) { + if (obj instanceof MutableByte) { + return value == ((MutableByte) obj).byteValue(); + } + return false; + } + + @Override + public int hashCode() { + return value; + } + + // ----------------------------------------------------------------------- + /** + * 比较 + * + * @param other 其它 MutableByte 对象 + * @return x==y返回0,x<y返回-1,x>y返回1 + */ + @Override + public int compareTo(final MutableByte other) { + return NumberUtil.compare(this.value, other.value); + } + + // ----------------------------------------------------------------------- + @Override + public String toString() { + return String.valueOf(value); + } + +} diff --git a/src/main/java/cn/hutool/core/lang/mutable/MutableDouble.java b/src/main/java/cn/hutool/core/lang/mutable/MutableDouble.java new file mode 100644 index 0000000..17702fb --- /dev/null +++ b/src/main/java/cn/hutool/core/lang/mutable/MutableDouble.java @@ -0,0 +1,192 @@ +package cn.hutool.core.lang.mutable; + +import cn.hutool.core.util.NumberUtil; + +/** + * 可变 {@code double} 类型 + * + * @see Double + * @since 3.0.1 + */ +public class MutableDouble extends Number implements Comparable, Mutable { + private static final long serialVersionUID = 1L; + + private double value; + + /** + * 构造,默认值0 + */ + public MutableDouble() { + } + + /** + * 构造 + * @param value 值 + */ + public MutableDouble(final double value) { + this.value = value; + } + + /** + * 构造 + * @param value 值 + */ + public MutableDouble(final Number value) { + this(value.doubleValue()); + } + + /** + * 构造 + * @param value String值 + * @throws NumberFormatException 数字转换错误 + */ + public MutableDouble(final String value) throws NumberFormatException { + this.value = Double.parseDouble(value); + } + + @Override + public Double get() { + return this.value; + } + + /** + * 设置值 + * @param value 值 + */ + public void set(final double value) { + this.value = value; + } + + @Override + public void set(final Number value) { + this.value = value.doubleValue(); + } + + // ----------------------------------------------------------------------- + /** + * 值+1 + * @return this + */ + public MutableDouble increment() { + value++; + return this; + } + + /** + * 值减一 + * @return this + */ + public MutableDouble decrement() { + value--; + return this; + } + + // ----------------------------------------------------------------------- + /** + * 增加值 + * @param operand 被增加的值 + * @return this + */ + public MutableDouble add(final double operand) { + this.value += operand; + return this; + } + + /** + * 增加值 + * @param operand 被增加的值,非空 + * @return this + */ + public MutableDouble add(final Number operand) { + this.value += operand.doubleValue(); + return this; + } + + /** + * 减去值 + * + * @param operand 被减的值 + * @return this + */ + public MutableDouble subtract(final double operand) { + this.value -= operand; + return this; + } + + /** + * 减去值 + * + * @param operand 被减的值,非空 + * @return this + */ + public MutableDouble subtract(final Number operand) { + this.value -= operand.doubleValue(); + return this; + } + + // ----------------------------------------------------------------------- + @Override + public int intValue() { + return (int) value; + } + + @Override + public long longValue() { + return (long) value; + } + + @Override + public float floatValue() { + return (float) value; + } + + @Override + public double doubleValue() { + return value; + } + + // ----------------------------------------------------------------------- + /** + * 相等需同时满足如下条件: + *
    + *
  1. 非空
  2. + *
  3. 类型为 {@code MutableDouble}
  4. + *
  5. 值相等
  6. + *
+ * + * @param obj 比对的对象 + * @return 相同返回true,否则 {@code false} + */ + @Override + public boolean equals(final Object obj) { + if (obj instanceof MutableDouble) { + return (Double.doubleToLongBits(((MutableDouble)obj).value) == Double.doubleToLongBits(value)); + } + return false; + } + + @Override + public int hashCode() { + final long bits = Double.doubleToLongBits(value); + return (int) (bits ^ bits >>> 32); + } + + // ----------------------------------------------------------------------- + /** + * 比较 + * + * @param other 其它 {@code MutableDouble} 对象 + * @return x==y返回0,x<y返回-1,x>y返回1 + */ + @Override + public int compareTo(final MutableDouble other) { + return NumberUtil.compare(this.value, other.value); + } + + // ----------------------------------------------------------------------- + @Override + public String toString() { + return String.valueOf(value); + } + +} diff --git a/src/main/java/cn/hutool/core/lang/mutable/MutableFloat.java b/src/main/java/cn/hutool/core/lang/mutable/MutableFloat.java new file mode 100644 index 0000000..55763ac --- /dev/null +++ b/src/main/java/cn/hutool/core/lang/mutable/MutableFloat.java @@ -0,0 +1,193 @@ +package cn.hutool.core.lang.mutable; + +import cn.hutool.core.util.NumberUtil; + +/** + * 可变 float 类型 + * + * @see Float + * @since 3.0.1 + */ +public class MutableFloat extends Number implements Comparable, Mutable { + private static final long serialVersionUID = 1L; + + private float value; + + /** + * 构造,默认值0 + */ + public MutableFloat() { + } + + /** + * 构造 + * @param value 值 + */ + public MutableFloat(final float value) { + this.value = value; + } + + /** + * 构造 + * @param value 值 + */ + public MutableFloat(final Number value) { + this(value.floatValue()); + } + + /** + * 构造 + * @param value String值 + * @throws NumberFormatException 数字转换错误 + */ + public MutableFloat(final String value) throws NumberFormatException { + this.value = Float.parseFloat(value); + } + + @Override + public Float get() { + return this.value; + } + + /** + * 设置值 + * @param value 值 + */ + public void set(final float value) { + this.value = value; + } + + @Override + public void set(final Number value) { + this.value = value.floatValue(); + } + + // ----------------------------------------------------------------------- + /** + * 值+1 + * @return this + */ + public MutableFloat increment() { + value++; + return this; + } + + /** + * 值减一 + * @return this + */ + public MutableFloat decrement() { + value--; + return this; + } + + // ----------------------------------------------------------------------- + /** + * 增加值 + * @param operand 被增加的值 + * @return this + */ + public MutableFloat add(final float operand) { + this.value += operand; + return this; + } + + /** + * 增加值 + * @param operand 被增加的值,非空 + * @return this + * @throws NullPointerException if the object is null + */ + public MutableFloat add(final Number operand) { + this.value += operand.floatValue(); + return this; + } + + /** + * 减去值 + * + * @param operand 被减的值 + * @return this + */ + public MutableFloat subtract(final float operand) { + this.value -= operand; + return this; + } + + /** + * 减去值 + * + * @param operand 被减的值,非空 + * @return this + * @throws NullPointerException if the object is null + */ + public MutableFloat subtract(final Number operand) { + this.value -= operand.floatValue(); + return this; + } + + // ----------------------------------------------------------------------- + @Override + public int intValue() { + return (int) value; + } + + @Override + public long longValue() { + return (long) value; + } + + @Override + public float floatValue() { + return value; + } + + @Override + public double doubleValue() { + return value; + } + + // ----------------------------------------------------------------------- + /** + * 相等需同时满足如下条件: + *
    + *
  1. 非空
  2. + *
  3. 类型为 {@link MutableFloat}
  4. + *
  5. 值相等
  6. + *
+ * + * @param obj 比对的对象 + * @return 相同返回true,否则 false + */ + @Override + public boolean equals(final Object obj) { + if (obj instanceof MutableFloat) { + return (Float.floatToIntBits(((MutableFloat)obj).value) == Float.floatToIntBits(value)); + } + return false; + } + + @Override + public int hashCode() { + return Float.floatToIntBits(value); + } + + // ----------------------------------------------------------------------- + /** + * 比较 + * + * @param other 其它 {@link MutableFloat} 对象 + * @return x==y返回0,x<y返回-1,x>y返回1 + */ + @Override + public int compareTo(final MutableFloat other) { + return NumberUtil.compare(this.value, other.value); + } + + // ----------------------------------------------------------------------- + @Override + public String toString() { + return String.valueOf(value); + } + +} diff --git a/src/main/java/cn/hutool/core/lang/mutable/MutableInt.java b/src/main/java/cn/hutool/core/lang/mutable/MutableInt.java new file mode 100644 index 0000000..ae03343 --- /dev/null +++ b/src/main/java/cn/hutool/core/lang/mutable/MutableInt.java @@ -0,0 +1,193 @@ +package cn.hutool.core.lang.mutable; + +import cn.hutool.core.util.NumberUtil; + +/** + * 可变 int 类型 + * + * @see Integer + * @since 3.0.1 + */ +public class MutableInt extends Number implements Comparable, Mutable { + private static final long serialVersionUID = 1L; + + private int value; + + /** + * 构造,默认值0 + */ + public MutableInt() { + } + + /** + * 构造 + * @param value 值 + */ + public MutableInt(final int value) { + this.value = value; + } + + /** + * 构造 + * @param value 值 + */ + public MutableInt(final Number value) { + this(value.intValue()); + } + + /** + * 构造 + * @param value String值 + * @throws NumberFormatException 数字转换错误 + */ + public MutableInt(final String value) throws NumberFormatException { + this.value = Integer.parseInt(value); + } + + @Override + public Integer get() { + return this.value; + } + + /** + * 设置值 + * @param value 值 + */ + public void set(final int value) { + this.value = value; + } + + @Override + public void set(final Number value) { + this.value = value.intValue(); + } + + // ----------------------------------------------------------------------- + /** + * 值+1 + * @return this + */ + public MutableInt increment() { + value++; + return this; + } + + /** + * 值减一 + * @return this + */ + public MutableInt decrement() { + value--; + return this; + } + + // ----------------------------------------------------------------------- + /** + * 增加值 + * @param operand 被增加的值 + * @return this + */ + public MutableInt add(final int operand) { + this.value += operand; + return this; + } + + /** + * 增加值 + * @param operand 被增加的值,非空 + * @return this + * @throws NullPointerException if the object is null + */ + public MutableInt add(final Number operand) { + this.value += operand.intValue(); + return this; + } + + /** + * 减去值 + * + * @param operand 被减的值 + * @return this + */ + public MutableInt subtract(final int operand) { + this.value -= operand; + return this; + } + + /** + * 减去值 + * + * @param operand 被减的值,非空 + * @return this + * @throws NullPointerException if the object is null + */ + public MutableInt subtract(final Number operand) { + this.value -= operand.intValue(); + return this; + } + + // ----------------------------------------------------------------------- + @Override + public int intValue() { + return value; + } + + @Override + public long longValue() { + return value; + } + + @Override + public float floatValue() { + return value; + } + + @Override + public double doubleValue() { + return value; + } + + // ----------------------------------------------------------------------- + /** + * 相等需同时满足如下条件: + *
    + *
  1. 非空
  2. + *
  3. 类型为 MutableInt
  4. + *
  5. 值相等
  6. + *
+ * + * @param obj 比对的对象 + * @return 相同返回true,否则 {@code false} + */ + @Override + public boolean equals(final Object obj) { + if (obj instanceof MutableInt) { + return value == ((MutableInt) obj).intValue(); + } + return false; + } + + @Override + public int hashCode() { + return this.value; + } + + // ----------------------------------------------------------------------- + /** + * 比较 + * + * @param other 其它 MutableInt 对象 + * @return x==y返回0,x<y返回-1,x>y返回1 + */ + @Override + public int compareTo(final MutableInt other) { + return NumberUtil.compare(this.value, other.value); + } + + // ----------------------------------------------------------------------- + @Override + public String toString() { + return String.valueOf(value); + } + +} diff --git a/src/main/java/cn/hutool/core/lang/mutable/MutableLong.java b/src/main/java/cn/hutool/core/lang/mutable/MutableLong.java new file mode 100644 index 0000000..5895325 --- /dev/null +++ b/src/main/java/cn/hutool/core/lang/mutable/MutableLong.java @@ -0,0 +1,205 @@ +package cn.hutool.core.lang.mutable; + +import cn.hutool.core.util.NumberUtil; + +/** + * 可变 {@code long} 类型 + * + * @see Long + * @since 3.0.1 + */ +public class MutableLong extends Number implements Comparable, Mutable { + private static final long serialVersionUID = 1L; + + private long value; + + /** + * 构造,默认值0 + */ + public MutableLong() { + } + + /** + * 构造 + * + * @param value 值 + */ + public MutableLong(final long value) { + this.value = value; + } + + /** + * 构造 + * + * @param value 值 + */ + public MutableLong(final Number value) { + this(value.longValue()); + } + + /** + * 构造 + * + * @param value String值 + * @throws NumberFormatException 数字转换错误 + */ + public MutableLong(final String value) throws NumberFormatException { + this.value = Long.parseLong(value); + } + + @Override + public Long get() { + return this.value; + } + + /** + * 设置值 + * + * @param value 值 + */ + public void set(final long value) { + this.value = value; + } + + @Override + public void set(final Number value) { + this.value = value.longValue(); + } + + // ----------------------------------------------------------------------- + + /** + * 值+1 + * + * @return this + */ + public MutableLong increment() { + value++; + return this; + } + + /** + * 值减一 + * + * @return this + */ + public MutableLong decrement() { + value--; + return this; + } + + // ----------------------------------------------------------------------- + + /** + * 增加值 + * + * @param operand 被增加的值 + * @return this + */ + public MutableLong add(final long operand) { + this.value += operand; + return this; + } + + /** + * 增加值 + * + * @param operand 被增加的值,非空 + * @return this + * @throws NullPointerException if the object is null + */ + public MutableLong add(final Number operand) { + this.value += operand.longValue(); + return this; + } + + /** + * 减去值 + * + * @param operand 被减的值 + * @return this + */ + public MutableLong subtract(final long operand) { + this.value -= operand; + return this; + } + + /** + * 减去值 + * + * @param operand 被减的值,非空 + * @return this + * @throws NullPointerException if the object is null + */ + public MutableLong subtract(final Number operand) { + this.value -= operand.longValue(); + return this; + } + + // ----------------------------------------------------------------------- + @Override + public int intValue() { + return (int) value; + } + + @Override + public long longValue() { + return value; + } + + @Override + public float floatValue() { + return value; + } + + @Override + public double doubleValue() { + return value; + } + + // ----------------------------------------------------------------------- + + /** + * 相等需同时满足如下条件: + *
    + *
  1. 非空
  2. + *
  3. 类型为 MutableLong
  4. + *
  5. 值相等
  6. + *
+ * + * @param obj 比对的对象 + * @return 相同返回true,否则 {@code false} + */ + @Override + public boolean equals(final Object obj) { + if (obj instanceof MutableLong) { + return value == ((MutableLong) obj).longValue(); + } + return false; + } + + @Override + public int hashCode() { + return (int) (value ^ (value >>> 32)); + } + + // ----------------------------------------------------------------------- + + /** + * 比较 + * + * @param other 其它 MutableLong 对象 + * @return x==y返回0,x<y返回-1,x>y返回1 + */ + @Override + public int compareTo(final MutableLong other) { + return NumberUtil.compare(this.value, other.value); + } + + // ----------------------------------------------------------------------- + @Override + public String toString() { + return String.valueOf(value); + } + +} diff --git a/src/main/java/cn/hutool/core/lang/mutable/MutableObj.java b/src/main/java/cn/hutool/core/lang/mutable/MutableObj.java new file mode 100644 index 0000000..7fbc5e9 --- /dev/null +++ b/src/main/java/cn/hutool/core/lang/mutable/MutableObj.java @@ -0,0 +1,82 @@ +package cn.hutool.core.lang.mutable; + +import cn.hutool.core.util.ObjUtil; + +import java.io.Serializable; + +/** + * 可变{@code Object} + * + * @param 可变的类型 + * @since 3.0.1 + */ +public class MutableObj implements Mutable, Serializable { + private static final long serialVersionUID = 1L; + + /** + * 构建MutableObj + * @param value 被包装的值 + * @param 值类型 + * @return MutableObj + * @since 5.8.0 + */ + public static MutableObj of(T value){ + return new MutableObj<>(value); + } + + private T value; + + /** + * 构造,空值 + */ + public MutableObj() { + } + + /** + * 构造 + * + * @param value 值 + */ + public MutableObj(final T value) { + this.value = value; + } + + // ----------------------------------------------------------------------- + @Override + public T get() { + return this.value; + } + + @Override + public void set(final T value) { + this.value = value; + } + + // ----------------------------------------------------------------------- + @Override + public boolean equals(final Object obj) { + if (obj == null) { + return false; + } + if (this == obj) { + return true; + } + if (this.getClass() == obj.getClass()) { + final MutableObj that = (MutableObj) obj; + return ObjUtil.equals(this.value, that.value); + } + return false; + } + + @Override + public int hashCode() { + return value == null ? 0 : value.hashCode(); + } + + // ----------------------------------------------------------------------- + @Override + public String toString() { + return value == null ? "null" : value.toString(); + } + +} diff --git a/src/main/java/cn/hutool/core/lang/mutable/MutablePair.java b/src/main/java/cn/hutool/core/lang/mutable/MutablePair.java new file mode 100644 index 0000000..7e55385 --- /dev/null +++ b/src/main/java/cn/hutool/core/lang/mutable/MutablePair.java @@ -0,0 +1,57 @@ +package cn.hutool.core.lang.mutable; + +import cn.hutool.core.lang.Pair; + +/** + * 可变{@link Pair}实现,可以修改键和值 + * + * @param 键类型 + * @param 值类型 + * @since 5.7.16 + */ +public class MutablePair extends Pair implements Mutable>{ + private static final long serialVersionUID = 1L; + + /** + * 构造 + * + * @param key 键 + * @param value 值 + */ + public MutablePair(K key, V value) { + super(key, value); + } + + /** + * 设置键 + * + * @param key 新键 + * @return this + */ + public MutablePair setKey(K key) { + this.key = key; + return this; + } + + /** + * 设置值 + * + * @param value 新值 + * @return this + */ + public MutablePair setValue(V value) { + this.value = value; + return this; + } + + @Override + public Pair get() { + return this; + } + + @Override + public void set(Pair pair) { + this.key = pair.getKey(); + this.value = pair.getValue(); + } +} diff --git a/src/main/java/cn/hutool/core/lang/mutable/MutableShort.java b/src/main/java/cn/hutool/core/lang/mutable/MutableShort.java new file mode 100644 index 0000000..7067263 --- /dev/null +++ b/src/main/java/cn/hutool/core/lang/mutable/MutableShort.java @@ -0,0 +1,198 @@ +package cn.hutool.core.lang.mutable; + +import cn.hutool.core.util.NumberUtil; + +/** + * 可变 short 类型 + * + * @see Short + * @since 3.0.1 + */ +public class MutableShort extends Number implements Comparable, Mutable { + private static final long serialVersionUID = 1L; + + private short value; + + /** + * 构造,默认值0 + */ + public MutableShort() { + } + + /** + * 构造 + * @param value 值 + */ + public MutableShort(final short value) { + this.value = value; + } + + /** + * 构造 + * @param value 值 + */ + public MutableShort(final Number value) { + this(value.shortValue()); + } + + /** + * 构造 + * @param value String值 + * @throws NumberFormatException 转为Short错误 + */ + public MutableShort(final String value) throws NumberFormatException { + this.value = Short.parseShort(value); + } + + @Override + public Short get() { + return this.value; + } + + /** + * 设置值 + * @param value 值 + */ + public void set(final short value) { + this.value = value; + } + + @Override + public void set(final Number value) { + this.value = value.shortValue(); + } + + // ----------------------------------------------------------------------- + /** + * 值+1 + * @return this + */ + public MutableShort increment() { + value++; + return this; + } + + /** + * 值减一 + * @return this + */ + public MutableShort decrement() { + value--; + return this; + } + + // ----------------------------------------------------------------------- + /** + * 增加值 + * @param operand 被增加的值 + * @return this + */ + public MutableShort add(final short operand) { + this.value += operand; + return this; + } + + /** + * 增加值 + * @param operand 被增加的值,非空 + * @return this + * @throws NullPointerException if the object is null + */ + public MutableShort add(final Number operand) { + this.value += operand.shortValue(); + return this; + } + + /** + * 减去值 + * + * @param operand 被减的值 + * @return this + */ + public MutableShort subtract(final short operand) { + this.value -= operand; + return this; + } + + /** + * 减去值 + * + * @param operand 被减的值,非空 + * @return this + * @throws NullPointerException if the object is null + */ + public MutableShort subtract(final Number operand) { + this.value -= operand.shortValue(); + return this; + } + + // ----------------------------------------------------------------------- + @Override + public short shortValue() { + return value; + } + + @Override + public int intValue() { + return value; + } + + @Override + public long longValue() { + return value; + } + + @Override + public float floatValue() { + return value; + } + + @Override + public double doubleValue() { + return value; + } + + // ----------------------------------------------------------------------- + /** + * 相等需同时满足如下条件: + *
    + *
  1. 非空
  2. + *
  3. 类型为 {@link MutableShort}
  4. + *
  5. 值相等
  6. + *
+ * + * @param obj 比对的对象 + * @return 相同返回true,否则 false + */ + @Override + public boolean equals(final Object obj) { + if (obj instanceof MutableShort) { + return value == ((MutableShort) obj).shortValue(); + } + return false; + } + + @Override + public int hashCode() { + return value; + } + + // ----------------------------------------------------------------------- + /** + * 比较 + * + * @param other 其它 {@link MutableShort} 对象 + * @return x==y返回0,x<y返回-1,x>y返回1 + */ + @Override + public int compareTo(final MutableShort other) { + return NumberUtil.compare(this.value, other.value); + } + + // ----------------------------------------------------------------------- + @Override + public String toString() { + return String.valueOf(value); + } + +} diff --git a/src/main/java/cn/hutool/core/lang/mutable/package-info.java b/src/main/java/cn/hutool/core/lang/mutable/package-info.java new file mode 100644 index 0000000..f2a6e86 --- /dev/null +++ b/src/main/java/cn/hutool/core/lang/mutable/package-info.java @@ -0,0 +1,7 @@ +/** + * 提供可变值对象的封装,用于封装int、long等不可变值,使其可变 + * + * @author looly + * + */ +package cn.hutool.core.lang.mutable; \ No newline at end of file diff --git a/src/main/java/cn/hutool/core/lang/package-info.java b/src/main/java/cn/hutool/core/lang/package-info.java new file mode 100644 index 0000000..21575f3 --- /dev/null +++ b/src/main/java/cn/hutool/core/lang/package-info.java @@ -0,0 +1,7 @@ +/** + * 语言特性包,包括大量便捷的数据结构,例如验证器Validator,分布式ID生成器Snowflake等 + * + * @author looly + * + */ +package cn.hutool.core.lang; \ No newline at end of file diff --git a/src/main/java/cn/hutool/core/lang/reflect/ActualTypeMapperPool.java b/src/main/java/cn/hutool/core/lang/reflect/ActualTypeMapperPool.java new file mode 100644 index 0000000..b13e12f --- /dev/null +++ b/src/main/java/cn/hutool/core/lang/reflect/ActualTypeMapperPool.java @@ -0,0 +1,118 @@ +package cn.hutool.core.lang.reflect; + +import cn.hutool.core.convert.Convert; +import cn.hutool.core.map.WeakConcurrentMap; +import cn.hutool.core.util.TypeUtil; + +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.lang.reflect.TypeVariable; +import java.util.HashMap; +import java.util.Map; + +/** + * 泛型变量和泛型实际类型映射关系缓存 + * + * @author looly + * @since 5.4.2 + */ +public class ActualTypeMapperPool { + + private static final WeakConcurrentMap> CACHE = new WeakConcurrentMap<>(); + + /** + * 获取泛型变量和泛型实际类型的对应关系Map + * + * @param type 被解析的包含泛型参数的类 + * @return 泛型对应关系Map + */ + public static Map get(Type type) { + return CACHE.computeIfAbsent(type, (key) -> createTypeMap(type)); + } + + /** + * 获取泛型变量名(字符串)和泛型实际类型的对应关系Map + * + * @param type 被解析的包含泛型参数的类 + * @return 泛型对应关系Map + * @since 5.7.16 + */ + public static Map getStrKeyMap(Type type){ + return Convert.toMap(String.class, Type.class, get(type)); + } + + /** + * 获得泛型变量对应的泛型实际类型,如果此变量没有对应的实际类型,返回null + * + * @param type 类 + * @param typeVariable 泛型变量,例如T等 + * @return 实际类型,可能为Class等 + */ + public static Type getActualType(Type type, TypeVariable typeVariable) { + final Map typeTypeMap = get(type); + Type result = typeTypeMap.get(typeVariable); + while (result instanceof TypeVariable) { + result = typeTypeMap.get(result); + } + return result; + } + + /** + * 获取指定泛型变量对应的真实类型
+ * 由于子类中泛型参数实现和父类(接口)中泛型定义位置是一一对应的,因此可以通过对应关系找到泛型实现类型
+ * + * @param type 真实类型所在类,此类中记录了泛型参数对应的实际类型 + * @param typeVariables 泛型变量,需要的实际类型对应的泛型参数 + * @return 给定泛型参数对应的实际类型,如果无对应类型,对应位置返回null + */ + public static Type[] getActualTypes(Type type, Type... typeVariables) { + // 查找方法定义所在类或接口中此泛型参数的位置 + final Type[] result = new Type[typeVariables.length]; + for (int i = 0; i < typeVariables.length; i++) { + result[i] = (typeVariables[i] instanceof TypeVariable) + ? getActualType(type, (TypeVariable) typeVariables[i]) + : typeVariables[i]; + } + return result; + } + + /** + * 创建类中所有的泛型变量和泛型实际类型的对应关系Map + * + * @param type 被解析的包含泛型参数的类 + * @return 泛型对应关系Map + */ + private static Map createTypeMap(Type type) { + final Map typeMap = new HashMap<>(); + + // 按继承层级寻找泛型变量和实际类型的对应关系 + // 在类中,对应关系分为两类: + // 1. 父类定义变量,子类标注实际类型 + // 2. 父类定义变量,子类继承这个变量,让子类的子类去标注,以此类推 + // 此方法中我们将每一层级的对应关系全部加入到Map中,查找实际类型的时候,根据传入的泛型变量, + // 找到对应关系,如果对应的是继承的泛型变量,则递归继续找,直到找到实际或返回null为止。 + // 如果传入的非Class,例如TypeReference,获取到泛型参数中实际的泛型对象类,继续按照类处理 + while (null != type) { + final ParameterizedType parameterizedType = TypeUtil.toParameterizedType(type); + if(null == parameterizedType){ + break; + } + final Type[] typeArguments = parameterizedType.getActualTypeArguments(); + final Class rawType = (Class) parameterizedType.getRawType(); + final Type[] typeParameters = rawType.getTypeParameters(); + + Type value; + for (int i = 0; i < typeParameters.length; i++) { + value = typeArguments[i]; + // 跳过泛型变量对应泛型变量的情况 + if(!(value instanceof TypeVariable)){ + typeMap.put(typeParameters[i], value); + } + } + + type = rawType; + } + + return typeMap; + } +} diff --git a/src/main/java/cn/hutool/core/lang/reflect/LookupFactory.java b/src/main/java/cn/hutool/core/lang/reflect/LookupFactory.java new file mode 100644 index 0000000..5ae8392 --- /dev/null +++ b/src/main/java/cn/hutool/core/lang/reflect/LookupFactory.java @@ -0,0 +1,76 @@ +package cn.hutool.core.lang.reflect; + +import cn.hutool.core.exceptions.UtilException; + +import java.lang.invoke.MethodHandles; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +/** + * {@link MethodHandles.Lookup}工厂,用于创建{@link MethodHandles.Lookup}对象
+ * jdk8中如果直接调用{@link MethodHandles#lookup()}获取到的{@link MethodHandles.Lookup}在调用findSpecial和unreflectSpecial + * 时会出现权限不够问题,抛出"no private access for invokespecial"异常,因此针对JDK8及JDK9+分别封装lookup方法。 + * + * 参考: + *

https://blog.csdn.net/u013202238/article/details/108687086

+ * + * @author looly + * @since 5.7.7 + */ +public class LookupFactory { + + private static final int ALLOWED_MODES = MethodHandles.Lookup.PRIVATE | MethodHandles.Lookup.PROTECTED + | MethodHandles.Lookup.PACKAGE | MethodHandles.Lookup.PUBLIC; + + private static Constructor java8LookupConstructor; + private static Method privateLookupInMethod; + + static { + //先查询jdk9 开始提供的java.lang.invoke.MethodHandles.privateLookupIn方法, + //如果没有说明是jdk8的版本.(不考虑jdk8以下版本) + try { + //noinspection JavaReflectionMemberAccess + privateLookupInMethod = MethodHandles.class.getMethod("privateLookupIn", Class.class, MethodHandles.Lookup.class); + } catch (NoSuchMethodException ignore) { + //ignore + } + + //jdk8 + //这种方式其实也适用于jdk9及以上的版本,但是上面优先,可以避免 jdk9 反射警告 + if (privateLookupInMethod == null) { + try { + java8LookupConstructor = MethodHandles.Lookup.class.getDeclaredConstructor(Class.class, int.class); + java8LookupConstructor.setAccessible(true); + } catch (NoSuchMethodException e) { + //可能是jdk8 以下版本 + throw new IllegalStateException( + "There is neither 'privateLookupIn(Class, Lookup)' nor 'Lookup(Class, int)' method in java.lang.invoke.MethodHandles.", e); + } + } + } + + /** + * jdk8中如果直接调用{@link MethodHandles#lookup()}获取到的{@link MethodHandles.Lookup}在调用findSpecial和unreflectSpecial + * 时会出现权限不够问题,抛出"no private access for invokespecial"异常,因此针对JDK8及JDK9+分别封装lookup方法。 + * + * @param callerClass 被调用的类或接口 + * @return {@link MethodHandles.Lookup} + */ + public static MethodHandles.Lookup lookup(Class callerClass) { + //使用反射,因为当前jdk可能不是java9或以上版本 + if (privateLookupInMethod != null) { + try { + return (MethodHandles.Lookup) privateLookupInMethod.invoke(MethodHandles.class, callerClass, MethodHandles.lookup()); + } catch (IllegalAccessException | InvocationTargetException e) { + throw new UtilException(e); + } + } + //jdk 8 + try { + return java8LookupConstructor.newInstance(callerClass, ALLOWED_MODES); + } catch (Exception e) { + throw new IllegalStateException("no 'Lookup(Class, int)' method in java.lang.invoke.MethodHandles.", e); + } + } +} diff --git a/src/main/java/cn/hutool/core/lang/reflect/MethodHandleUtil.java b/src/main/java/cn/hutool/core/lang/reflect/MethodHandleUtil.java new file mode 100644 index 0000000..fbfa2de --- /dev/null +++ b/src/main/java/cn/hutool/core/lang/reflect/MethodHandleUtil.java @@ -0,0 +1,227 @@ +package cn.hutool.core.lang.reflect; + +import cn.hutool.core.exceptions.UtilException; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.ReflectUtil; +import cn.hutool.core.util.StrUtil; + +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; +import java.lang.reflect.Method; + +/** + * 方法句柄{@link MethodHandle}封装工具类
+ * 方法句柄是一个有类型的,可以直接执行的指向底层方法、构造器、field等的引用,可以简单理解为函数指针,它是一种更加底层的查找、调整和调用方法的机制。 + * 参考: + *
    + *
  • https://stackoverflow.com/questions/22614746/how-do-i-invoke-java-8-default-methods-reflectively
  • + *
+ * + * @author looly + * @since 5.7.7 + */ +public class MethodHandleUtil { + + /** + * jdk8中如果直接调用{@link MethodHandles#lookup()}获取到的{@link MethodHandles.Lookup}在调用findSpecial和unreflectSpecial + * 时会出现权限不够问题,抛出"no private access for invokespecial"异常,因此针对JDK8及JDK9+分别封装lookup方法。 + * + * @param callerClass 被调用的类或接口 + * @return {@link MethodHandles.Lookup} + */ + public static MethodHandles.Lookup lookup(Class callerClass) { + return LookupFactory.lookup(callerClass); + } + + /** + * 查找指定方法的方法句柄
+ * 此方法只会查找: + *
    + *
  • 当前类的方法(包括构造方法和private方法)
  • + *
  • 父类的方法(包括构造方法和private方法)
  • + *
  • 当前类的static方法
  • + *
+ * + * @param callerClass 方法所在类或接口 + * @param name 方法名称,{@code null}或者空则查找构造方法 + * @param type 返回类型和参数类型 + * @return 方法句柄 {@link MethodHandle},{@code null}表示未找到方法 + */ + public static MethodHandle findMethod(Class callerClass, String name, MethodType type) { + if (StrUtil.isBlank(name)) { + return findConstructor(callerClass, type); + } + + MethodHandle handle = null; + final MethodHandles.Lookup lookup = lookup(callerClass); + try { + handle = lookup.findVirtual(callerClass, name, type); + } catch (IllegalAccessException | NoSuchMethodException ignore) { + //ignore + } + + // static方法 + if (null == handle) { + try { + handle = lookup.findStatic(callerClass, name, type); + } catch (IllegalAccessException | NoSuchMethodException ignore) { + //ignore + } + } + + // 特殊方法,包括构造方法、私有方法等 + if (null == handle) { + try { + handle = lookup.findSpecial(callerClass, name, type, callerClass); + } catch (NoSuchMethodException ignore) { + //ignore + } catch (IllegalAccessException e) { + throw new UtilException(e); + } + } + + return handle; + } + + /** + * 查找指定的构造方法 + * + * @param callerClass 类 + * @param args 参数 + * @return 构造方法句柄 + */ + public static MethodHandle findConstructor(Class callerClass, Class... args) { + return findConstructor(callerClass, MethodType.methodType(void.class, args)); + } + + /** + * 查找指定的构造方法 + * + * @param callerClass 类 + * @param type 参数类型,此处返回类型应为void.class + * @return 构造方法句柄 + */ + public static MethodHandle findConstructor(Class callerClass, MethodType type) { + final MethodHandles.Lookup lookup = lookup(callerClass); + try { + return lookup.findConstructor(callerClass, type); + } catch (NoSuchMethodException e) { + return null; + } catch (IllegalAccessException e) { + throw new UtilException(e); + } + } + + /** + * 执行接口或对象中的特殊方法(private、static等)
+ * + *
+	 *     interface Duck {
+	 *         default String quack() {
+	 *             return "Quack";
+	 *         }
+	 *     }
+	 *
+	 *     Duck duck = (Duck) Proxy.newProxyInstance(
+	 *         ClassLoaderUtil.getClassLoader(),
+	 *         new Class[] { Duck.class },
+	 *         MethodHandleUtil::invokeDefault);
+	 * 
+ * + * @param 返回结果类型 + * @param obj 接口的子对象或代理对象 + * @param methodName 方法名称 + * @param args 参数 + * @return 结果 + */ + public static T invokeSpecial(Object obj, String methodName, Object... args) { + Assert.notNull(obj, "Object to get method must be not null!"); + Assert.notBlank(methodName, "Method name must be not blank!"); + + final Method method = ReflectUtil.getMethodOfObj(obj, methodName, args); + if (null == method) { + throw new UtilException("No such method: [{}] from [{}]", methodName, obj.getClass()); + } + return invokeSpecial(obj, method, args); + } + + /** + * 执行接口或对象中的方法 + * + * @param 返回结果类型 + * @param obj 接口的子对象或代理对象 + * @param method 方法 + * @param args 参数 + * @return 结果 + */ + public static T invoke(Object obj, Method method, Object... args) { + return invoke(false, obj, method, args); + } + + /** + * 执行接口或对象中的特殊方法(private、static等)
+ * + *
+	 *     interface Duck {
+	 *         default String quack() {
+	 *             return "Quack";
+	 *         }
+	 *     }
+	 *
+	 *     Duck duck = (Duck) Proxy.newProxyInstance(
+	 *         ClassLoaderUtil.getClassLoader(),
+	 *         new Class[] { Duck.class },
+	 *         MethodHandleUtil::invoke);
+	 * 
+ * + * @param 返回结果类型 + * @param obj 接口的子对象或代理对象 + * @param method 方法 + * @param args 参数 + * @return 结果 + */ + public static T invokeSpecial(Object obj, Method method, Object... args) { + return invoke(true, obj, method, args); + } + + /** + * 执行接口或对象中的方法
+ * + *
+	 *     interface Duck {
+	 *         default String quack() {
+	 *             return "Quack";
+	 *         }
+	 *     }
+	 *
+	 *     Duck duck = (Duck) Proxy.newProxyInstance(
+	 *         ClassLoaderUtil.getClassLoader(),
+	 *         new Class[] { Duck.class },
+	 *         MethodHandleUtil::invoke);
+	 * 
+ * + * @param 返回结果类型 + * @param isSpecial 是否为特殊方法(private、static等) + * @param obj 接口的子对象或代理对象 + * @param method 方法 + * @param args 参数 + * @return 结果 + */ + @SuppressWarnings("unchecked") + public static T invoke(boolean isSpecial, Object obj, Method method, Object... args) { + Assert.notNull(method, "Method must be not null!"); + final Class declaringClass = method.getDeclaringClass(); + final MethodHandles.Lookup lookup = lookup(declaringClass); + try { + MethodHandle handle = isSpecial ? lookup.unreflectSpecial(method, declaringClass) + : lookup.unreflect(method); + if (null != obj) { + handle = handle.bindTo(obj); + } + return (T) handle.invokeWithArguments(args); + } catch (Throwable e) { + throw new UtilException(e); + } + } +} diff --git a/src/main/java/cn/hutool/core/lang/reflect/package-info.java b/src/main/java/cn/hutool/core/lang/reflect/package-info.java new file mode 100644 index 0000000..918772a --- /dev/null +++ b/src/main/java/cn/hutool/core/lang/reflect/package-info.java @@ -0,0 +1,7 @@ +/** + * 提供反射相关功能对象和类 + * + * @author looly + * @since 5.4.2 + */ +package cn.hutool.core.lang.reflect; \ No newline at end of file diff --git a/src/main/java/cn/hutool/core/lang/tree/Node.java b/src/main/java/cn/hutool/core/lang/tree/Node.java new file mode 100644 index 0000000..901feea --- /dev/null +++ b/src/main/java/cn/hutool/core/lang/tree/Node.java @@ -0,0 +1,86 @@ +package cn.hutool.core.lang.tree; + +import cn.hutool.core.comparator.CompareUtil; + +import java.io.Serializable; + +/** + * 节点接口,提供节点相关的的方法定义 + * + * @param ID类型 + * @author looly + * @since 5.2.4 + */ +public interface Node extends Comparable>, Serializable { + + /** + * 获取ID + * + * @return ID + */ + T getId(); + + /** + * 设置ID + * + * @param id ID + * @return this + */ + Node setId(T id); + + /** + * 获取父节点ID + * + * @return 父节点ID + */ + T getParentId(); + + /** + * 设置父节点ID + * + * @param parentId 父节点ID + * @return this + */ + Node setParentId(T parentId); + + /** + * 获取节点标签名称 + * + * @return 节点标签名称 + */ + CharSequence getName(); + + /** + * 设置节点标签名称 + * + * @param name 节点标签名称 + * @return this + */ + Node setName(CharSequence name); + + /** + * 获取权重 + * + * @return 权重 + */ + Comparable getWeight(); + + /** + * 设置权重 + * + * @param weight 权重 + * @return this + */ + Node setWeight(Comparable weight); + + @SuppressWarnings({"unchecked", "rawtypes", "NullableProblems"}) + @Override + default int compareTo(Node node) { + if(null == node){ + return 1; + } + final Comparable weight = this.getWeight(); + final Comparable weightOther = node.getWeight(); + return CompareUtil.compare(weight, weightOther); + } +} diff --git a/src/main/java/cn/hutool/core/lang/tree/Tree.java b/src/main/java/cn/hutool/core/lang/tree/Tree.java new file mode 100644 index 0000000..34775f9 --- /dev/null +++ b/src/main/java/cn/hutool/core/lang/tree/Tree.java @@ -0,0 +1,355 @@ +package cn.hutool.core.lang.tree; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.lang.Filter; +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.CharUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.function.Consumer; + +/** + * 通过转换器将你的实体转化为TreeNodeMap节点实体 属性都存在此处,属性有序,可支持排序 + * + * @param ID类型 + * @author liangbaikai + * @since 5.2.1 + */ +public class Tree extends LinkedHashMap implements Node { + private static final long serialVersionUID = 1L; + + private final TreeNodeConfig treeNodeConfig; + private Tree parent; + + public Tree() { + this(null); + } + + /** + * 构造 + * + * @param treeNodeConfig TreeNode配置 + */ + public Tree(TreeNodeConfig treeNodeConfig) { + this.treeNodeConfig = ObjectUtil.defaultIfNull( + treeNodeConfig, TreeNodeConfig.DEFAULT_CONFIG); + } + + /** + * 获取节点配置 + * + * @return 节点配置 + * @since 5.7.2 + */ + public TreeNodeConfig getConfig() { + return this.treeNodeConfig; + } + + /** + * 获取父节点 + * + * @return 父节点 + * @since 5.2.4 + */ + public Tree getParent() { + return parent; + } + + /** + * 获取ID对应的节点,如果有多个ID相同的节点,只返回第一个。
+ * 此方法只查找此节点及子节点,采用广度优先遍历。 + * + * @param id ID + * @return 节点 + * @since 5.2.4 + */ + public Tree getNode(T id) { + return TreeUtil.getNode(this, id); + } + + /** + * 获取所有父节点名称列表 + * + *

+ * 比如有个人在研发1部,他上面有研发部,接着上面有技术中心
+ * 返回结果就是:[研发一部, 研发中心, 技术中心] + * + * @param id 节点ID + * @param includeCurrentNode 是否包含当前节点的名称 + * @return 所有父节点名称列表 + * @since 5.2.4 + */ + public List getParentsName(T id, boolean includeCurrentNode) { + return TreeUtil.getParentsName(getNode(id), includeCurrentNode); + } + + /** + * 获取所有父节点名称列表 + * + *

+ * 比如有个人在研发1部,他上面有研发部,接着上面有技术中心
+ * 返回结果就是:[研发一部, 研发中心, 技术中心] + * + * @param includeCurrentNode 是否包含当前节点的名称 + * @return 所有父节点名称列表 + * @since 5.2.4 + */ + public List getParentsName(boolean includeCurrentNode) { + return TreeUtil.getParentsName(this, includeCurrentNode); + } + + /** + * 设置父节点 + * + * @param parent 父节点 + * @return this + * @since 5.2.4 + */ + public Tree setParent(Tree parent) { + this.parent = parent; + if (null != parent) { + this.setParentId(parent.getId()); + } + return this; + } + + @Override + @SuppressWarnings("unchecked") + public T getId() { + return (T) this.get(treeNodeConfig.getIdKey()); + } + + @Override + public Tree setId(T id) { + this.put(treeNodeConfig.getIdKey(), id); + return this; + } + + @Override + @SuppressWarnings("unchecked") + public T getParentId() { + return (T) this.get(treeNodeConfig.getParentIdKey()); + } + + @Override + public Tree setParentId(T parentId) { + this.put(treeNodeConfig.getParentIdKey(), parentId); + return this; + } + + @Override + public CharSequence getName() { + return (CharSequence) this.get(treeNodeConfig.getNameKey()); + } + + @Override + public Tree setName(CharSequence name) { + this.put(treeNodeConfig.getNameKey(), name); + return this; + } + + @Override + public Comparable getWeight() { + return (Comparable) this.get(treeNodeConfig.getWeightKey()); + } + + @Override + public Tree setWeight(Comparable weight) { + this.put(treeNodeConfig.getWeightKey(), weight); + return this; + } + + /** + * 获取所有子节点 + * + * @return 所有子节点 + */ + @SuppressWarnings("unchecked") + public List> getChildren() { + return (List>) this.get(treeNodeConfig.getChildrenKey()); + } + + /** + * 是否有子节点,无子节点则此为叶子节点 + * + * @return 是否有子节点 + * @since 5.7.17 + */ + public boolean hasChild() { + return CollUtil.isNotEmpty(getChildren()); + } + + /** + * 递归树并处理子树下的节点: + * + * @param consumer 节点处理器 + * @since 5.7.16 + */ + public void walk(Consumer> consumer) { + consumer.accept(this); + final List> children = getChildren(); + if (CollUtil.isNotEmpty(children)) { + children.forEach((tree) -> tree.walk(consumer)); + } + } + + /** + * 递归过滤并生成新的树
+ * 通过{@link Filter}指定的过滤规则,本节点或子节点满足过滤条件,则保留当前节点,否则抛弃节点及其子节点 + * + * @param filter 节点过滤规则函数,只需处理本级节点本身即可 + * @return 过滤后的节点,{@code null} 表示不满足过滤要求,丢弃之 + * @see #filter(Filter) + * @since 5.7.17 + */ + public Tree filterNew(Filter> filter) { + return cloneTree().filter(filter); + } + + /** + * 递归过滤当前树,注意此方法会修改当前树
+ * 通过{@link Filter}指定的过滤规则,本节点或子节点满足过滤条件,则保留当前节点及其所有子节点,否则抛弃节点及其子节点 + * + * @param filter 节点过滤规则函数,只需处理本级节点本身即可 + * @return 过滤后的节点,{@code null} 表示不满足过滤要求,丢弃之 + * @see #filterNew(Filter) + * @since 5.7.17 + */ + public Tree filter(Filter> filter) { + if(filter.accept(this)){ + // 本节点满足,则包括所有子节点都保留 + return this; + } + + final List> children = getChildren(); + if (CollUtil.isNotEmpty(children)) { + // 递归过滤子节点 + final List> filteredChildren = new ArrayList<>(children.size()); + Tree filteredChild; + for (Tree child : children) { + filteredChild = child.filter(filter); + if (null != filteredChild) { + filteredChildren.add(filteredChild); + } + } + if(CollUtil.isNotEmpty(filteredChildren)){ + // 子节点有符合过滤条件的节点,则本节点保留 + return this.setChildren(filteredChildren); + } else { + this.setChildren(null); + } + } + + // 子节点都不符合过滤条件,检查本节点 + return null; + } + + /** + * 设置子节点,设置后会覆盖所有原有子节点 + * + * @param children 子节点列表,如果为{@code null}表示移除子节点 + * @return this + */ + public Tree setChildren(List> children) { + if(null == children){ + this.remove(treeNodeConfig.getChildrenKey()); + } + this.put(treeNodeConfig.getChildrenKey(), children); + return this; + } + + /** + * 增加子节点,同时关联子节点的父节点为当前节点 + * + * @param children 子节点列表 + * @return this + * @since 5.6.7 + */ + @SafeVarargs + public final Tree addChildren(Tree... children) { + if (ArrayUtil.isNotEmpty(children)) { + List> childrenList = this.getChildren(); + if (null == childrenList) { + childrenList = new ArrayList<>(); + setChildren(childrenList); + } + for (Tree child : children) { + child.setParent(this); + childrenList.add(child); + } + } + return this; + } + + /** + * 扩展属性 + * + * @param key 键 + * @param value 扩展值 + */ + public void putExtra(String key, Object value) { + Assert.notEmpty(key, "Key must be not empty !"); + this.put(key, value); + } + + @Override + public String toString() { + final StringWriter stringWriter = new StringWriter(); + printTree(this, new PrintWriter(stringWriter), 0); + return stringWriter.toString(); + } + + /** + * 递归克隆当前节点(即克隆整个树,保留字段值)
+ * 注意,此方法只会克隆节点,节点属性如果是引用类型,不会克隆 + * + * @return 新的节点 + * @since 5.7.17 + */ + public Tree cloneTree() { + final Tree result = ObjectUtil.clone(this); + result.setChildren(cloneChildren()); + return result; + } + + /** + * 递归复制子节点 + * + * @return 新的子节点列表 + */ + private List> cloneChildren() { + final List> children = getChildren(); + if (null == children) { + return null; + } + final List> newChildren = new ArrayList<>(children.size()); + children.forEach((t) -> newChildren.add(t.cloneTree())); + return newChildren; + } + + /** + * 打印 + * + * @param tree 树 + * @param writer Writer + * @param intent 缩进量 + */ + private static void printTree(Tree tree, PrintWriter writer, int intent) { + writer.println(StrUtil.format("{}{}[{}]", StrUtil.repeat(CharUtil.SPACE, intent), tree.getName(), tree.getId())); + writer.flush(); + + final List> children = tree.getChildren(); + if (CollUtil.isNotEmpty(children)) { + for (Tree child : children) { + printTree(child, writer, intent + 2); + } + } + } +} diff --git a/src/main/java/cn/hutool/core/lang/tree/TreeBuilder.java b/src/main/java/cn/hutool/core/lang/tree/TreeBuilder.java new file mode 100644 index 0000000..226c954 --- /dev/null +++ b/src/main/java/cn/hutool/core/lang/tree/TreeBuilder.java @@ -0,0 +1,307 @@ +package cn.hutool.core.lang.tree; + +import cn.hutool.core.builder.Builder; +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.lang.tree.parser.NodeParser; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.ObjectUtil; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * 树构建器 + * + * @param ID类型 + */ +public class TreeBuilder implements Builder> { + private static final long serialVersionUID = 1L; + + private final Tree root; + private final Map> idTreeMap; + private boolean isBuild; + + /** + * 创建Tree构建器 + * + * @param rootId 根节点ID + * @param ID类型 + * @return TreeBuilder + */ + public static TreeBuilder of(T rootId) { + return of(rootId, null); + } + + /** + * 创建Tree构建器 + * + * @param rootId 根节点ID + * @param config 配置 + * @param ID类型 + * @return TreeBuilder + */ + public static TreeBuilder of(T rootId, TreeNodeConfig config) { + return new TreeBuilder<>(rootId, config); + } + + /** + * 构造 + * + * @param rootId 根节点ID + * @param config 配置 + */ + public TreeBuilder(E rootId, TreeNodeConfig config) { + root = new Tree<>(config); + root.setId(rootId); + this.idTreeMap = new LinkedHashMap<>(); + } + + /** + * 设置ID + * + * @param id ID + * @return this + * @since 5.7.14 + */ + public TreeBuilder setId(E id) { + this.root.setId(id); + return this; + } + + /** + * 设置父节点ID + * + * @param parentId 父节点ID + * @return this + * @since 5.7.14 + */ + public TreeBuilder setParentId(E parentId) { + this.root.setParentId(parentId); + return this; + } + + /** + * 设置节点标签名称 + * + * @param name 节点标签名称 + * @return this + * @since 5.7.14 + */ + public TreeBuilder setName(CharSequence name) { + this.root.setName(name); + return this; + } + + /** + * 设置权重 + * + * @param weight 权重 + * @return this + * @since 5.7.14 + */ + public TreeBuilder setWeight(Comparable weight) { + this.root.setWeight(weight); + return this; + } + + /** + * 扩展属性 + * + * @param key 键 + * @param value 扩展值 + * @return this + * @since 5.7.14 + */ + public TreeBuilder putExtra(String key, Object value) { + Assert.notEmpty(key, "Key must be not empty !"); + this.root.put(key, value); + return this; + } + + /** + * 增加节点列表,增加的节点是不带子节点的 + * + * @param map 节点列表 + * @return this + */ + public TreeBuilder append(Map> map) { + checkBuilt(); + + this.idTreeMap.putAll(map); + return this; + } + + /** + * 增加节点列表,增加的节点是不带子节点的 + * + * @param trees 节点列表 + * @return this + */ + public TreeBuilder append(Iterable> trees) { + checkBuilt(); + + for (Tree tree : trees) { + this.idTreeMap.put(tree.getId(), tree); + } + return this; + } + + /** + * 增加节点列表,增加的节点是不带子节点的 + * + * @param list Bean列表 + * @param Bean类型 + * @param nodeParser 节点转换器,用于定义一个Bean如何转换为Tree节点 + * @return this + */ + public TreeBuilder append(List list, NodeParser nodeParser) { + return append(list, null, nodeParser); + } + + /** + * 增加节点列表,增加的节点是不带子节点的 + * + * @param Bean类型 + * @param list Bean列表 + * @param rootId 根ID + * @param nodeParser 节点转换器,用于定义一个Bean如何转换为Tree节点 + * @return this + * @since 5.8.6 + */ + public TreeBuilder append(List list, E rootId, NodeParser nodeParser) { + checkBuilt(); + + final TreeNodeConfig config = this.root.getConfig(); + final Map> map = new LinkedHashMap<>(list.size(), 1); + Tree node; + for (T t : list) { + node = new Tree<>(config); + nodeParser.parse(t, node); + if (null != rootId && !rootId.getClass().equals(node.getId().getClass())) { + throw new IllegalArgumentException("rootId type is node.getId().getClass()!"); + } + map.put(node.getId(), node); + } + return append(map); + } + + /** + * 重置Builder,实现复用 + * + * @return this + */ + public TreeBuilder reset() { + this.idTreeMap.clear(); + this.root.setChildren(null); + this.isBuild = false; + return this; + } + + @Override + public Tree build() { + checkBuilt(); + + buildFromMap(); + cutTree(); + + this.isBuild = true; + this.idTreeMap.clear(); + + return root; + } + + /** + * 构建树列表,没有顶层节点,例如: + * + *

+	 * -用户管理
+	 *  -用户管理
+	 *    +用户添加
+	 * - 部门管理
+	 *  -部门管理
+	 *    +部门添加
+	 * 
+ * + * @return 树列表 + */ + public List> buildList() { + if (isBuild) { + // 已经构建过了 + return this.root.getChildren(); + } + return build().getChildren(); + } + + /** + * 开始构建 + */ + private void buildFromMap() { + if (MapUtil.isEmpty(this.idTreeMap)) { + return; + } + + final Map> eTreeMap = MapUtil.sortByValue(this.idTreeMap, false); + E parentId; + for (Tree node : eTreeMap.values()) { + if (null == node) { + continue; + } + parentId = node.getParentId(); + if (ObjectUtil.equals(this.root.getId(), parentId)) { + this.root.addChildren(node); + continue; + } + + final Tree parentNode = eTreeMap.get(parentId); + if (null != parentNode) { + parentNode.addChildren(node); + } + } + } + + /** + * 树剪枝 + */ + private void cutTree() { + final TreeNodeConfig config = this.root.getConfig(); + final Integer deep = config.getDeep(); + if (null == deep || deep < 0) { + return; + } + cutTree(this.root, 0, deep); + } + + /** + * 树剪枝叶 + * + * @param tree 节点 + * @param currentDepp 当前层级 + * @param maxDeep 最大层级 + */ + private void cutTree(Tree tree, int currentDepp, int maxDeep) { + if (null == tree) { + return; + } + if (currentDepp == maxDeep) { + // 剪枝 + tree.setChildren(null); + return; + } + + final List> children = tree.getChildren(); + if (CollUtil.isNotEmpty(children)) { + for (Tree child : children) { + cutTree(child, currentDepp + 1, maxDeep); + } + } + } + + /** + * 检查是否已经构建 + */ + private void checkBuilt() { + Assert.isFalse(isBuild, "Current tree has been built."); + } +} diff --git a/src/main/java/cn/hutool/core/lang/tree/TreeNode.java b/src/main/java/cn/hutool/core/lang/tree/TreeNode.java new file mode 100644 index 0000000..034cc8c --- /dev/null +++ b/src/main/java/cn/hutool/core/lang/tree/TreeNode.java @@ -0,0 +1,150 @@ +package cn.hutool.core.lang.tree; + + +import java.util.Map; +import java.util.Objects; + +/** + * 树节点 每个属性都可以在{@link TreeNodeConfig}中被重命名
+ * 在你的项目里它可以是部门实体、地区实体等任意类树节点实体 + * 类树节点实体: 包含key,父Key.不限于这些属性的可以构造成一颗树的实体对象 + * + * @param ID类型 + * @author liangbaikai + */ +public class TreeNode implements Node { + private static final long serialVersionUID = 1L; + + /** + * ID + */ + private T id; + + /** + * 父节点ID + */ + private T parentId; + + /** + * 名称 + */ + private CharSequence name; + + /** + * 顺序 越小优先级越高 默认0 + */ + private Comparable weight = 0; + + /** + * 扩展字段 + */ + private Map extra; + + + /** + * 空构造 + */ + public TreeNode() { + } + + /** + * 构造 + * + * @param id ID + * @param parentId 父节点ID + * @param name 名称 + * @param weight 权重 + */ + public TreeNode(T id, T parentId, String name, Comparable weight) { + this.id = id; + this.parentId = parentId; + this.name = name; + if (weight != null) { + this.weight = weight; + } + + } + + @Override + public T getId() { + return id; + } + + @Override + public TreeNode setId(T id) { + this.id = id; + return this; + } + + @Override + public T getParentId() { + return this.parentId; + } + + @Override + public TreeNode setParentId(T parentId) { + this.parentId = parentId; + return this; + } + + @Override + public CharSequence getName() { + return name; + } + + @Override + public TreeNode setName(CharSequence name) { + this.name = name; + return this; + } + + @Override + public Comparable getWeight() { + return weight; + } + + @Override + public TreeNode setWeight(Comparable weight) { + this.weight = weight; + return this; + } + + /** + * 获取扩展字段 + * + * @return 扩展字段Map + * @since 5.2.5 + */ + public Map getExtra() { + return extra; + } + + /** + * 设置扩展字段 + * + * @param extra 扩展字段 + * @return this + * @since 5.2.5 + */ + public TreeNode setExtra(Map extra) { + this.extra = extra; + return this; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + TreeNode treeNode = (TreeNode) o; + return Objects.equals(id, treeNode.id); + } + + @Override + public int hashCode() { + return Objects.hash(id); + } +} diff --git a/src/main/java/cn/hutool/core/lang/tree/TreeNodeConfig.java b/src/main/java/cn/hutool/core/lang/tree/TreeNodeConfig.java new file mode 100644 index 0000000..1bc9422 --- /dev/null +++ b/src/main/java/cn/hutool/core/lang/tree/TreeNodeConfig.java @@ -0,0 +1,148 @@ +package cn.hutool.core.lang.tree; + +import java.io.Serializable; + +/** + * 树配置属性相关 + * + * @author liangbaikai + */ +public class TreeNodeConfig implements Serializable { + private static final long serialVersionUID = 1L; + + /** + * 默认属性配置对象 + */ + public static TreeNodeConfig DEFAULT_CONFIG = new TreeNodeConfig(); + + // 属性名配置字段 + private String idKey = "id"; + private String parentIdKey = "parentId"; + private String weightKey = "weight"; + private String nameKey = "name"; + private String childrenKey = "children"; + // 可以配置递归深度 从0开始计算 默认此配置为空,即不限制 + private Integer deep; + + + /** + * 获取ID对应的名称 + * + * @return ID对应的名称 + */ + public String getIdKey() { + return this.idKey; + } + + /** + * 设置ID对应的名称 + * + * @param idKey ID对应的名称 + * @return this + */ + public TreeNodeConfig setIdKey(String idKey) { + this.idKey = idKey; + return this; + } + + /** + * 获取权重对应的名称 + * + * @return 权重对应的名称 + */ + public String getWeightKey() { + return this.weightKey; + } + + /** + * 设置权重对应的名称 + * + * @param weightKey 权重对应的名称 + * @return this + */ + public TreeNodeConfig setWeightKey(String weightKey) { + this.weightKey = weightKey; + return this; + } + + /** + * 获取节点名对应的名称 + * + * @return 节点名对应的名称 + */ + public String getNameKey() { + return this.nameKey; + } + + /** + * 设置节点名对应的名称 + * + * @param nameKey 节点名对应的名称 + * @return this + */ + public TreeNodeConfig setNameKey(String nameKey) { + this.nameKey = nameKey; + return this; + } + + /** + * 获取子点对应的名称 + * + * @return 子点对应的名称 + */ + public String getChildrenKey() { + return this.childrenKey; + } + + /** + * 设置子点对应的名称 + * + * @param childrenKey 子点对应的名称 + * @return this + */ + public TreeNodeConfig setChildrenKey(String childrenKey) { + this.childrenKey = childrenKey; + return this; + } + + /** + * 获取父节点ID对应的名称 + * + * @return 父点对应的名称 + */ + public String getParentIdKey() { + return this.parentIdKey; + } + + + /** + * 设置父点对应的名称 + * + * @param parentIdKey 父点对应的名称 + * @return this + */ + public TreeNodeConfig setParentIdKey(String parentIdKey) { + this.parentIdKey = parentIdKey; + return this; + } + + /** + * 获取递归深度 + * + * @return 递归深度 + */ + public Integer getDeep() { + return this.deep; + } + + /** + * 设置递归深度 + * + * @param deep 递归深度 + * @return this + */ + public TreeNodeConfig setDeep(Integer deep) { + this.deep = deep; + return this; + } +} diff --git a/src/main/java/cn/hutool/core/lang/tree/TreeUtil.java b/src/main/java/cn/hutool/core/lang/tree/TreeUtil.java new file mode 100644 index 0000000..fb69fb2 --- /dev/null +++ b/src/main/java/cn/hutool/core/lang/tree/TreeUtil.java @@ -0,0 +1,239 @@ +package cn.hutool.core.lang.tree; + +import cn.hutool.core.collection.IterUtil; +import cn.hutool.core.lang.tree.parser.DefaultNodeParser; +import cn.hutool.core.lang.tree.parser.NodeParser; +import cn.hutool.core.util.ObjectUtil; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** + * 树工具类 + * + * @author liangbaikai + */ +public class TreeUtil { + + /** + * 构建单root节点树 + * + * @param list 源数据集合 + * @return {@link Tree} + * @since 5.7.2 + */ + public static Tree buildSingle(List> list) { + return buildSingle(list, 0); + } + + /** + * 树构建 + * + * @param list 源数据集合 + * @return List + */ + public static List> build(List> list) { + return build(list, 0); + } + + /** + * 构建单root节点树
+ * 它会生成一个以指定ID为ID的空的节点,然后逐级增加子节点。 + * + * @param ID类型 + * @param list 源数据集合 + * @param parentId 最顶层父id值 一般为 0 之类 + * @return {@link Tree} + * @since 5.7.2 + */ + public static Tree buildSingle(List> list, E parentId) { + return buildSingle(list, parentId, TreeNodeConfig.DEFAULT_CONFIG, new DefaultNodeParser<>()); + } + + /** + * 树构建 + * + * @param ID类型 + * @param list 源数据集合 + * @param parentId 最顶层父id值 一般为 0 之类 + * @return List + */ + public static List> build(List> list, E parentId) { + return build(list, parentId, TreeNodeConfig.DEFAULT_CONFIG, new DefaultNodeParser<>()); + } + + /** + * 构建单root节点树
+ * 它会生成一个以指定ID为ID的空的节点,然后逐级增加子节点。 + * + * @param 转换的实体 为数据源里的对象类型 + * @param ID类型 + * @param list 源数据集合 + * @param parentId 最顶层父id值 一般为 0 之类 + * @param nodeParser 转换器 + * @return {@link Tree} + * @since 5.7.2 + */ + public static Tree buildSingle(List list, E parentId, NodeParser nodeParser) { + return buildSingle(list, parentId, TreeNodeConfig.DEFAULT_CONFIG, nodeParser); + } + + /** + * 树构建 + * + * @param 转换的实体 为数据源里的对象类型 + * @param ID类型 + * @param list 源数据集合 + * @param parentId 最顶层父id值 一般为 0 之类 + * @param nodeParser 转换器 + * @return List + */ + public static List> build(List list, E parentId, NodeParser nodeParser) { + return build(list, parentId, TreeNodeConfig.DEFAULT_CONFIG, nodeParser); + } + + /** + * 树构建 + * + * @param 转换的实体 为数据源里的对象类型 + * @param ID类型 + * @param list 源数据集合 + * @param rootId 最顶层父id值 一般为 0 之类 + * @param treeNodeConfig 配置 + * @param nodeParser 转换器 + * @return List + */ + public static List> build(List list, E rootId, TreeNodeConfig treeNodeConfig, NodeParser nodeParser) { + return buildSingle(list, rootId, treeNodeConfig, nodeParser).getChildren(); + } + + /** + * 构建单root节点树
+ * 它会生成一个以指定ID为ID的空的节点,然后逐级增加子节点。 + * + * @param 转换的实体 为数据源里的对象类型 + * @param ID类型 + * @param list 源数据集合 + * @param rootId 最顶层父id值 一般为 0 之类 + * @param treeNodeConfig 配置 + * @param nodeParser 转换器 + * @return {@link Tree} + * @since 5.7.2 + */ + public static Tree buildSingle(List list, E rootId, TreeNodeConfig treeNodeConfig, NodeParser nodeParser) { + return TreeBuilder.of(rootId, treeNodeConfig) + .append(list, rootId, nodeParser).build(); + } + + /** + * 树构建,按照权重排序 + * + * @param ID类型 + * @param map 源数据Map + * @param rootId 最顶层父id值 一般为 0 之类 + * @return List + * @since 5.6.7 + */ + public static List> build(Map> map, E rootId) { + return buildSingle(map, rootId).getChildren(); + } + + /** + * 单点树构建,按照权重排序
+ * 它会生成一个以指定ID为ID的空的节点,然后逐级增加子节点。 + * + * @param ID类型 + * @param map 源数据Map + * @param rootId 根节点id值 一般为 0 之类 + * @return {@link Tree} + * @since 5.7.2 + */ + public static Tree buildSingle(Map> map, E rootId) { + final Tree tree = IterUtil.getFirstNoneNull(map.values()); + if (null != tree) { + final TreeNodeConfig config = tree.getConfig(); + return TreeBuilder.of(rootId, config) + .append(map) + .build(); + } + + return createEmptyNode(rootId); + } + + /** + * 获取ID对应的节点,如果有多个ID相同的节点,只返回第一个。
+ * 此方法只查找此节点及子节点,采用递归深度优先遍历。 + * + * @param ID类型 + * @param node 节点 + * @param id ID + * @return 节点 + * @since 5.2.4 + */ + public static Tree getNode(Tree node, T id) { + if (ObjectUtil.equal(id, node.getId())) { + return node; + } + + final List> children = node.getChildren(); + if (null == children) { + return null; + } + + // 查找子节点 + Tree childNode; + for (Tree child : children) { + childNode = child.getNode(id); + if (null != childNode) { + return childNode; + } + } + + // 未找到节点 + return null; + } + + /** + * 获取所有父节点名称列表 + * + *

+ * 比如有个人在研发1部,他上面有研发部,接着上面有技术中心
+ * 返回结果就是:[研发一部, 研发中心, 技术中心] + * + * @param 节点ID类型 + * @param node 节点 + * @param includeCurrentNode 是否包含当前节点的名称 + * @return 所有父节点名称列表,node为null返回空List + * @since 5.2.4 + */ + public static List getParentsName(Tree node, boolean includeCurrentNode) { + final List result = new ArrayList<>(); + if (null == node) { + return result; + } + + if (includeCurrentNode) { + result.add(node.getName()); + } + + Tree parent = node.getParent(); + while (null != parent) { + result.add(parent.getName()); + parent = parent.getParent(); + } + return result; + } + + /** + * 创建空Tree的节点 + * + * @param id 节点ID + * @param 节点ID类型 + * @return {@link Tree} + * @since 5.7.2 + */ + public static Tree createEmptyNode(E id) { + return new Tree().setId(id); + } +} diff --git a/src/main/java/cn/hutool/core/lang/tree/package-info.java b/src/main/java/cn/hutool/core/lang/tree/package-info.java new file mode 100644 index 0000000..efb648a --- /dev/null +++ b/src/main/java/cn/hutool/core/lang/tree/package-info.java @@ -0,0 +1,14 @@ +/** + * 提供通用树生成,特点:

+ * 1、每个字段可自定义
+ * 2、支持排序 树深度配置,自定义转换器等
+ * 3、支持额外属性扩展
+ * 4、贴心 许多属性,特性都有默认值处理
+ * 5、使用简单 可一行代码生成树
+ * 6、代码简洁轻量无额外依赖 + *

+ * + * @author liangbaikai(https://gitee.com/liangbaikai00/) + * @since 5.2.1 + */ +package cn.hutool.core.lang.tree; \ No newline at end of file diff --git a/src/main/java/cn/hutool/core/lang/tree/parser/DefaultNodeParser.java b/src/main/java/cn/hutool/core/lang/tree/parser/DefaultNodeParser.java new file mode 100644 index 0000000..f5b38ad --- /dev/null +++ b/src/main/java/cn/hutool/core/lang/tree/parser/DefaultNodeParser.java @@ -0,0 +1,30 @@ +package cn.hutool.core.lang.tree.parser; + +import cn.hutool.core.lang.tree.TreeNode; +import cn.hutool.core.lang.tree.Tree; +import cn.hutool.core.map.MapUtil; + +import java.util.Map; + +/** + * 默认的简单转换器 + * + * @param ID类型 + * @author liangbaikai + */ +public class DefaultNodeParser implements NodeParser, T> { + + @Override + public void parse(TreeNode treeNode, Tree tree) { + tree.setId(treeNode.getId()); + tree.setParentId(treeNode.getParentId()); + tree.setWeight(treeNode.getWeight()); + tree.setName(treeNode.getName()); + + //扩展字段 + final Map extra = treeNode.getExtra(); + if(MapUtil.isNotEmpty(extra)){ + extra.forEach(tree::putExtra); + } + } +} diff --git a/src/main/java/cn/hutool/core/lang/tree/parser/NodeParser.java b/src/main/java/cn/hutool/core/lang/tree/parser/NodeParser.java new file mode 100644 index 0000000..ef240b8 --- /dev/null +++ b/src/main/java/cn/hutool/core/lang/tree/parser/NodeParser.java @@ -0,0 +1,19 @@ +package cn.hutool.core.lang.tree.parser; + +import cn.hutool.core.lang.tree.Tree; + +/** + * 树节点解析器 可以参考{@link DefaultNodeParser} + * + * @param 转换的实体 为数据源里的对象类型 + * @author liangbaikai + */ +@FunctionalInterface +public interface NodeParser { + /** + * @param object 源数据实体 + * @param treeNode 树节点实体 + */ + void parse(T object, Tree treeNode); +} + diff --git a/src/main/java/cn/hutool/core/map/AbsEntry.java b/src/main/java/cn/hutool/core/map/AbsEntry.java new file mode 100644 index 0000000..a2ab588 --- /dev/null +++ b/src/main/java/cn/hutool/core/map/AbsEntry.java @@ -0,0 +1,46 @@ +package cn.hutool.core.map; + +import cn.hutool.core.util.ObjectUtil; + +import java.util.Map; + +/** + * 抽象的{@link Map.Entry}实现,来自Guava
+ * 实现了默认的{@link #equals(Object)}、{@link #hashCode()}、{@link #toString()}方法。
+ * 默认{@link #setValue(Object)}抛出异常。 + * + * @param 键类型 + * @param 值类型 + * @author Guava + * @since 5.7.23 + */ +public abstract class AbsEntry implements Map.Entry { + + @Override + public V setValue(V value) { + throw new UnsupportedOperationException("Entry is read only."); + } + + @Override + public boolean equals(Object object) { + if (object instanceof Map.Entry) { + final Map.Entry that = (Map.Entry) object; + return ObjectUtil.equals(this.getKey(), that.getKey()) + && ObjectUtil.equals(this.getValue(), that.getValue()); + } + return false; + } + + @Override + public int hashCode() { + //copy from 1.8 HashMap.Node + K k = getKey(); + V v = getValue(); + return ((k == null) ? 0 : k.hashCode()) ^ ((v == null) ? 0 : v.hashCode()); + } + + @Override + public String toString() { + return getKey() + "=" + getValue(); + } +} diff --git a/src/main/java/cn/hutool/core/map/BiMap.java b/src/main/java/cn/hutool/core/map/BiMap.java new file mode 100644 index 0000000..3ce87cc --- /dev/null +++ b/src/main/java/cn/hutool/core/map/BiMap.java @@ -0,0 +1,133 @@ +package cn.hutool.core.map; + +import java.util.Map; +import java.util.function.BiFunction; +import java.util.function.Function; + +/** + * 双向Map
+ * 互换键值对不检查值是否有重复,如果有则后加入的元素替换先加入的元素
+ * 值的顺序在HashMap中不确定,所以谁覆盖谁也不确定,在有序的Map中按照先后顺序覆盖,保留最后的值
+ * 它与TableMap的区别是,BiMap维护两个Map实现高效的正向和反向查找 + * + * @param 键类型 + * @param 值类型 + * @since 5.2.6 + */ +public class BiMap extends MapWrapper { + private static final long serialVersionUID = 1L; + + private Map inverse; + + /** + * 构造 + * + * @param raw 被包装的Map + */ + public BiMap(Map raw) { + super(raw); + } + + @Override + public V put(K key, V value) { + if (null != this.inverse) { + this.inverse.put(value, key); + } + return super.put(key, value); + } + + @Override + public void putAll(Map m) { + super.putAll(m); + if (null != this.inverse) { + m.forEach((key, value) -> this.inverse.put(value, key)); + } + } + + @Override + public V remove(Object key) { + final V v = super.remove(key); + if (null != this.inverse && null != v) { + this.inverse.remove(v); + } + return v; + } + + @Override + public boolean remove(Object key, Object value) { + return super.remove(key, value) && null != this.inverse && this.inverse.remove(value, key); + } + + @Override + public void clear() { + super.clear(); + this.inverse = null; + } + + /** + * 获取反向Map + * + * @return 反向Map + */ + public Map getInverse() { + if (null == this.inverse) { + inverse = MapUtil.inverse(getRaw()); + } + return this.inverse; + } + + /** + * 根据值获得键 + * + * @param value 值 + * @return 键 + */ + public K getKey(V value) { + return getInverse().get(value); + } + + @Override + public V putIfAbsent(K key, V value) { + if (null != this.inverse) { + this.inverse.putIfAbsent(value, key); + } + return super.putIfAbsent(key, value); + } + + @Override + public V computeIfAbsent(K key, Function mappingFunction) { + final V result = super.computeIfAbsent(key, mappingFunction); + resetInverseMap(); + return result; + } + + @Override + public V computeIfPresent(K key, BiFunction remappingFunction) { + final V result = super.computeIfPresent(key, remappingFunction); + resetInverseMap(); + return result; + } + + @Override + public V compute(K key, BiFunction remappingFunction) { + final V result = super.compute(key, remappingFunction); + resetInverseMap(); + return result; + } + + @Override + public V merge(K key, V value, BiFunction remappingFunction) { + final V result = super.merge(key, value, remappingFunction); + resetInverseMap(); + return result; + } + + /** + * 重置反转的Map,如果反转map为空,则不操作。 + */ + private void resetInverseMap() { + if (null != this.inverse) { + inverse = null; + } + } +} diff --git a/src/main/java/cn/hutool/core/map/CamelCaseLinkedMap.java b/src/main/java/cn/hutool/core/map/CamelCaseLinkedMap.java new file mode 100644 index 0000000..639bfbf --- /dev/null +++ b/src/main/java/cn/hutool/core/map/CamelCaseLinkedMap.java @@ -0,0 +1,66 @@ +package cn.hutool.core.map; + +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * 驼峰Key风格的LinkedHashMap
+ * 对KEY转换为驼峰,get("int_value")和get("intValue")获得的值相同,put进入的值也会被覆盖 + * + * @author Looly + * + * @param 键类型 + * @param 值类型 + * @since 4.0.7 + */ +public class CamelCaseLinkedMap extends CamelCaseMap { + private static final long serialVersionUID = 4043263744224569870L; + + // ------------------------------------------------------------------------- Constructor start + /** + * 构造 + */ + public CamelCaseLinkedMap() { + this(DEFAULT_INITIAL_CAPACITY); + } + + /** + * 构造 + * + * @param initialCapacity 初始大小 + */ + public CamelCaseLinkedMap(int initialCapacity) { + this(initialCapacity, DEFAULT_LOAD_FACTOR); + } + + /** + * 构造 + * + * @param m Map + */ + public CamelCaseLinkedMap(Map m) { + this(DEFAULT_LOAD_FACTOR, m); + } + + /** + * 构造 + * + * @param loadFactor 加载因子 + * @param m Map,数据会被默认拷贝到一个新的LinkedHashMap中 + */ + public CamelCaseLinkedMap(float loadFactor, Map m) { + this(m.size(), loadFactor); + this.putAll(m); + } + + /** + * 构造 + * + * @param initialCapacity 初始大小 + * @param loadFactor 加载因子 + */ + public CamelCaseLinkedMap(int initialCapacity, float loadFactor) { + super(new LinkedHashMap<>(initialCapacity, loadFactor)); + } + // ------------------------------------------------------------------------- Constructor end +} diff --git a/src/main/java/cn/hutool/core/map/CamelCaseMap.java b/src/main/java/cn/hutool/core/map/CamelCaseMap.java new file mode 100644 index 0000000..fd71a49 --- /dev/null +++ b/src/main/java/cn/hutool/core/map/CamelCaseMap.java @@ -0,0 +1,87 @@ +package cn.hutool.core.map; + +import cn.hutool.core.util.StrUtil; + +import java.io.Serializable; +import java.util.HashMap; +import java.util.Map; +import java.util.function.Function; + +/** + * 驼峰Key风格的Map
+ * 对KEY转换为驼峰,get("int_value")和get("intValue")获得的值相同,put进入的值也会被覆盖 + * + * @param 键类型 + * @param 值类型 + * @author Looly + * @since 4.0.7 + */ +public class CamelCaseMap extends FuncKeyMap { + private static final long serialVersionUID = 4043263744224569870L; + + // ------------------------------------------------------------------------- Constructor start + + /** + * 构造 + */ + public CamelCaseMap() { + this(DEFAULT_INITIAL_CAPACITY); + } + + /** + * 构造 + * + * @param initialCapacity 初始大小 + */ + public CamelCaseMap(int initialCapacity) { + this(initialCapacity, DEFAULT_LOAD_FACTOR); + } + + /** + * 构造 + * + * @param m Map + */ + public CamelCaseMap(Map m) { + this(DEFAULT_LOAD_FACTOR, m); + } + + /** + * 构造 + * + * @param loadFactor 加载因子 + * @param m 初始Map,数据会被默认拷贝到一个新的HashMap中 + */ + public CamelCaseMap(float loadFactor, Map m) { + this(m.size(), loadFactor); + this.putAll(m); + } + + /** + * 构造 + * + * @param initialCapacity 初始大小 + * @param loadFactor 加载因子 + */ + public CamelCaseMap(int initialCapacity, float loadFactor) { + this(MapBuilder.create(new HashMap<>(initialCapacity, loadFactor))); + } + + /** + * 构造
+ * 注意此构造将传入的Map作为被包装的Map,针对任何修改,传入的Map都会被同样修改。 + * + * @param emptyMapBuilder Map构造器,必须构造空的Map + */ + CamelCaseMap(MapBuilder emptyMapBuilder) { + // issue#I5VRHW@Gitee 使Function可以被序列化 + super(emptyMapBuilder.build(), (Function & Serializable)(key) -> { + if (key instanceof CharSequence) { + key = StrUtil.toCamelCase(key.toString()); + } + //noinspection unchecked + return (K) key; + }); + } + // ------------------------------------------------------------------------- Constructor end +} diff --git a/src/main/java/cn/hutool/core/map/CaseInsensitiveLinkedMap.java b/src/main/java/cn/hutool/core/map/CaseInsensitiveLinkedMap.java new file mode 100644 index 0000000..5be588e --- /dev/null +++ b/src/main/java/cn/hutool/core/map/CaseInsensitiveLinkedMap.java @@ -0,0 +1,67 @@ +package cn.hutool.core.map; + +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * 忽略大小写的LinkedHashMap
+ * 对KEY忽略大小写,get("Value")和get("value")获得的值相同,put进入的值也会被覆盖 + * + * @author Looly + * + * @param 键类型 + * @param 值类型 + * @since 3.3.1 + */ +public class CaseInsensitiveLinkedMap extends CaseInsensitiveMap { + private static final long serialVersionUID = 4043263744224569870L; + + // ------------------------------------------------------------------------- Constructor start + /** + * 构造 + */ + public CaseInsensitiveLinkedMap() { + this(DEFAULT_INITIAL_CAPACITY); + } + + /** + * 构造 + * + * @param initialCapacity 初始大小 + */ + public CaseInsensitiveLinkedMap(int initialCapacity) { + this(initialCapacity, DEFAULT_LOAD_FACTOR); + } + + /** + * 构造 + * + * @param m Map + */ + public CaseInsensitiveLinkedMap(Map m) { + this(DEFAULT_LOAD_FACTOR, m); + } + + /** + * 构造 + * + * @param loadFactor 加载因子 + * @param m Map + * @since 3.1.2 + */ + public CaseInsensitiveLinkedMap(float loadFactor, Map m) { + this(m.size(), loadFactor); + this.putAll(m); + } + + /** + * 构造 + * + * @param initialCapacity 初始大小 + * @param loadFactor 加载因子 + */ + public CaseInsensitiveLinkedMap(int initialCapacity, float loadFactor) { + super(new LinkedHashMap<>(initialCapacity, loadFactor)); + } + // ------------------------------------------------------------------------- Constructor end +} diff --git a/src/main/java/cn/hutool/core/map/CaseInsensitiveMap.java b/src/main/java/cn/hutool/core/map/CaseInsensitiveMap.java new file mode 100644 index 0000000..23ff564 --- /dev/null +++ b/src/main/java/cn/hutool/core/map/CaseInsensitiveMap.java @@ -0,0 +1,87 @@ +package cn.hutool.core.map; + +import java.io.Serializable; +import java.util.HashMap; +import java.util.Map; +import java.util.function.Function; + +/** + * 忽略大小写的Map
+ * 对KEY忽略大小写,get("Value")和get("value")获得的值相同,put进入的值也会被覆盖 + * + * @author Looly + * + * @param 键类型 + * @param 值类型 + * @since 3.0.2 + */ +public class CaseInsensitiveMap extends FuncKeyMap { + private static final long serialVersionUID = 4043263744224569870L; + + //------------------------------------------------------------------------- Constructor start + /** + * 构造 + */ + public CaseInsensitiveMap() { + this(DEFAULT_INITIAL_CAPACITY); + } + + /** + * 构造 + * + * @param initialCapacity 初始大小 + */ + public CaseInsensitiveMap(int initialCapacity) { + this(initialCapacity, DEFAULT_LOAD_FACTOR); + } + + /** + * 构造
+ * 注意此构造将传入的Map作为被包装的Map,针对任何修改,传入的Map都会被同样修改。 + * + * @param m 被包装的自定义Map创建器 + */ + public CaseInsensitiveMap(Map m) { + this(DEFAULT_LOAD_FACTOR, m); + } + + /** + * 构造 + * + * @param loadFactor 加载因子 + * @param m Map + * @since 3.1.2 + */ + public CaseInsensitiveMap(float loadFactor, Map m) { + this(m.size(), loadFactor); + this.putAll(m); + } + + /** + * 构造 + * + * @param initialCapacity 初始大小 + * @param loadFactor 加载因子 + */ + public CaseInsensitiveMap(int initialCapacity, float loadFactor) { + this(MapBuilder.create(new HashMap<>(initialCapacity, loadFactor))); + } + + /** + * 构造
+ * 注意此构造将传入的Map作为被包装的Map,针对任何修改,传入的Map都会被同样修改。 + * + * @param emptyMapBuilder 被包装的自定义Map创建器 + */ + CaseInsensitiveMap(MapBuilder emptyMapBuilder) { + // issue#I5VRHW@Gitee 使Function可以被序列化 + super(emptyMapBuilder.build(), (Function & Serializable)(key)->{ + if (key instanceof CharSequence) { + key = key.toString().toLowerCase(); + } + //noinspection unchecked + return (K) key; + }); + } + //------------------------------------------------------------------------- Constructor end +} diff --git a/src/main/java/cn/hutool/core/map/CaseInsensitiveTreeMap.java b/src/main/java/cn/hutool/core/map/CaseInsensitiveTreeMap.java new file mode 100644 index 0000000..69f3ec5 --- /dev/null +++ b/src/main/java/cn/hutool/core/map/CaseInsensitiveTreeMap.java @@ -0,0 +1,59 @@ +package cn.hutool.core.map; + +import java.util.Comparator; +import java.util.Map; +import java.util.SortedMap; +import java.util.TreeMap; + +/** + * 忽略大小写的{@link TreeMap}
+ * 对KEY忽略大小写,get("Value")和get("value")获得的值相同,put进入的值也会被覆盖 + * + * @author Looly + * + * @param 键类型 + * @param 值类型 + * @since 3.3.1 + */ +public class CaseInsensitiveTreeMap extends CaseInsensitiveMap { + private static final long serialVersionUID = 4043263744224569870L; + + // ------------------------------------------------------------------------- Constructor start + /** + * 构造 + */ + public CaseInsensitiveTreeMap() { + this((Comparator) null); + } + + /** + * 构造 + * + * @param m Map + * @since 3.1.2 + */ + public CaseInsensitiveTreeMap(Map m) { + this(); + this.putAll(m); + } + + /** + * 构造 + * + * @param m Map,初始Map,键值对会被复制到新的TreeMap中 + * @since 3.1.2 + */ + public CaseInsensitiveTreeMap(SortedMap m) { + super(new TreeMap(m)); + } + + /** + * 构造 + * + * @param comparator 比较器,{@code null}表示使用默认比较器 + */ + public CaseInsensitiveTreeMap(Comparator comparator) { + super(new TreeMap<>(comparator)); + } + // ------------------------------------------------------------------------- Constructor end +} diff --git a/src/main/java/cn/hutool/core/map/CustomKeyMap.java b/src/main/java/cn/hutool/core/map/CustomKeyMap.java new file mode 100644 index 0000000..701f0d1 --- /dev/null +++ b/src/main/java/cn/hutool/core/map/CustomKeyMap.java @@ -0,0 +1,32 @@ +package cn.hutool.core.map; + +import java.util.Map; + +/** + * 自定义键的Map,默认HashMap实现 + * + * @param 键类型 + * @param 值类型 + * @author Looly + * @since 4.0.7 + */ +public abstract class CustomKeyMap extends TransMap { + private static final long serialVersionUID = 4043263744224569870L; + + /** + * 构造
+ * 通过传入一个Map从而确定Map的类型,子类需创建一个空的Map,而非传入一个已有Map,否则值可能会被修改 + * + * @param emptyMap Map 被包装的Map,必须为空Map,否则自定义key会无效 + * @since 3.1.2 + */ + public CustomKeyMap(Map emptyMap) { + super(emptyMap); + } + + @Override + protected V customValue(Object value) { + //noinspection unchecked + return (V)value; + } +} diff --git a/src/main/java/cn/hutool/core/map/FixedLinkedHashMap.java b/src/main/java/cn/hutool/core/map/FixedLinkedHashMap.java new file mode 100644 index 0000000..9d12386 --- /dev/null +++ b/src/main/java/cn/hutool/core/map/FixedLinkedHashMap.java @@ -0,0 +1,77 @@ +package cn.hutool.core.map; + +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.function.Consumer; + +/** + * 固定大小的{@link LinkedHashMap} 实现
+ * 注意此类非线程安全,由于{@link #get(Object)}操作会修改链表的顺序结构,因此也不可以使用读写锁。 + * + * @param 键类型 + * @param 值类型 + * @author looly + */ +public class FixedLinkedHashMap extends LinkedHashMap { + private static final long serialVersionUID = -629171177321416095L; + + /** + * 容量,超过此容量自动删除末尾元素 + */ + private int capacity; + /** + * 移除监听 + */ + private Consumer> removeListener; + + /** + * 构造 + * + * @param capacity 容量,实际初始容量比容量大1 + */ + public FixedLinkedHashMap(int capacity) { + super(capacity + 1, 1.0f, true); + this.capacity = capacity; + } + + /** + * 获取容量 + * + * @return 容量 + */ + public int getCapacity() { + return this.capacity; + } + + /** + * 设置容量 + * + * @param capacity 容量 + */ + public void setCapacity(int capacity) { + this.capacity = capacity; + } + + /** + * 设置自定义移除监听 + * + * @param removeListener 移除监听 + */ + public void setRemoveListener(final Consumer> removeListener) { + this.removeListener = removeListener; + } + + @Override + protected boolean removeEldestEntry(Map.Entry eldest) { + //当链表元素大于容量时,移除最老(最久未被使用)的元素 + if (size() > this.capacity) { + if (null != removeListener) { + // 自定义监听 + removeListener.accept(eldest); + } + return true; + } + return false; + } + +} diff --git a/src/main/java/cn/hutool/core/map/ForestMap.java b/src/main/java/cn/hutool/core/map/ForestMap.java new file mode 100644 index 0000000..6af809b --- /dev/null +++ b/src/main/java/cn/hutool/core/map/ForestMap.java @@ -0,0 +1,333 @@ +package cn.hutool.core.map; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.lang.Opt; +import cn.hutool.core.util.ObjectUtil; + +import java.util.Collection; +import java.util.Collections; +import java.util.Map; +import java.util.Set; +import java.util.function.BiConsumer; +import java.util.function.Function; + +/** + * 基于多个{@link TreeEntry}构成的、彼此平行的树结构构成的森林集合。 + * + * @param key类型 + * @param value类型 + * @author huangchengxing + * @see TreeEntry + */ +public interface ForestMap extends Map> { + + // ===================== Map接口方法的重定义 ===================== + + /** + * 添加一个节点,效果等同于 {@code putNode(key, node.getValue())} + *
    + *
  • 若key对应节点不存在,则以传入的键值创建一个新的节点;
  • + *
  • 若key对应节点存在,则将该节点的值替换为{@code node}指定的值;
  • + *
+ * + * @param key 节点的key值 + * @param node 节点 + * @return 节点,若key已有对应节点,则返回具有旧值的节点,否则返回null + * @see #putNode(Object, Object) + */ + @Override + default TreeEntry put(K key, TreeEntry node) { + return putNode(key, node.getValue()); + } + + /** + * 批量添加节点,若节点具有父节点或者子节点,则一并在当前实例中引入该关系 + * + * @param treeEntryMap 节点集合 + */ + @Override + default void putAll(Map> treeEntryMap) { + if (CollUtil.isEmpty(treeEntryMap)) { + return; + } + treeEntryMap.forEach((k, v) -> { + if (v.hasParent()) { + final TreeEntry parent = v.getDeclaredParent(); + putLinkedNodes(parent.getKey(), parent.getValue(), v.getKey(), v.getValue()); + } else { + putNode(v.getKey(), v.getValue()); + } + }); + } + + /** + * 将指定节点从当前{@link Map}中删除 + *
    + *
  • 若存在父节点或子节点,则将其断开其与父节点或子节点的引用关系;
  • + *
  • + * 若同时存在父节点或子节点,则会在删除后将让子节点直接成为父节点的子节点,比如:
    + * 现有引用关系 a -> b -> c,删除 b 后,将有 a -> c + *
  • + *
+ * + * @param key 节点的key + * @return 删除的节点,若key没有对应节点,则返回null + */ + @Override + TreeEntry remove(Object key); + + /** + * 将当前集合清空,并清除全部节点间的引用关系 + */ + @Override + void clear(); + + // ===================== 节点操作 ===================== + + /** + * 批量添加节点 + * + * @param 集合类型 + * @param values 要添加的值 + * @param keyGenerator 从值中获取key的方法 + * @param parentKeyGenerator 从值中获取父节点key的方法 + * @param ignoreNullNode 是否获取到的key为null的子节点/父节点 + */ + default > void putAllNode( + C values, Function keyGenerator, Function parentKeyGenerator, boolean ignoreNullNode) { + if (CollUtil.isEmpty(values)) { + return; + } + values.forEach(v -> { + final K key = keyGenerator.apply(v); + final K parentKey = parentKeyGenerator.apply(v); + + // 不忽略keu为null节点 + final boolean hasKey = ObjectUtil.isNotNull(key); + final boolean hasParentKey = ObjectUtil.isNotNull(parentKey); + if (!ignoreNullNode || (hasKey && hasParentKey)) { + linkNodes(parentKey, key); + get(key).setValue(v); + return; + } + + // 父子节点的key都为null + if (!hasKey && !hasParentKey) { + return; + } + + // 父节点key为null + if (hasKey) { + putNode(key, v); + return; + } + + // 子节点key为null + putNode(parentKey, null); + }); + } + + /** + * 添加一个节点 + *
    + *
  • 若key对应节点不存在,则以传入的键值创建一个新的节点;
  • + *
  • 若key对应节点存在,则将该节点的值替换为{@code node}指定的值;
  • + *
+ * + * @param key 节点的key + * @param value 节点的value + * @return 节点,若key已有对应节点,则返回具有旧值的节点,否则返回null + */ + TreeEntry putNode(K key, V value); + + /** + * 同时添加父子节点: + *
    + *
  • 若{@code parentKey}或{@code childKey}对应的节点不存在,则会根据键值创建一个对应的节点;
  • + *
  • 若{@code parentKey}或{@code childKey}对应的节点存在,则会更新对应节点的值;
  • + *
+ * 该操作等同于: + *
{@code
+	 *     putNode(parentKey, parentValue);
+	 *     putNode(childKey, childValue);
+	 *     linkNodes(parentKey, childKey);
+	 * }
+ * + * @param parentKey 父节点的key + * @param parentValue 父节点的value + * @param childKey 子节点的key + * @param childValue 子节点的值 + */ + default void putLinkedNodes(K parentKey, V parentValue, K childKey, V childValue) { + putNode(parentKey, parentValue); + putNode(childKey, childValue); + linkNodes(parentKey, childKey); + } + + /** + * 添加子节点,并为子节点指定父节点: + *
    + *
  • 若{@code parentKey}或{@code childKey}对应的节点不存在,则会根据键值创建一个对应的节点;
  • + *
  • 若{@code parentKey}或{@code childKey}对应的节点存在,则会更新对应节点的值;
  • + *
+ * + * @param parentKey 父节点的key + * @param childKey 子节点的key + * @param childValue 子节点的值 + */ + void putLinkedNodes(K parentKey, K childKey, V childValue); + + /** + * 为集合中的指定的节点建立父子关系 + * + * @param parentKey 父节点的key + * @param childKey 子节点的key + */ + default void linkNodes(K parentKey, K childKey) { + linkNodes(parentKey, childKey, null); + } + + /** + * 为集合中的指定的节点建立父子关系 + * + * @param parentKey 父节点的key + * @param childKey 子节点的key + * @param consumer 对父节点和子节点的操作,允许为null + */ + void linkNodes(K parentKey, K childKey, BiConsumer, TreeEntry> consumer); + + /** + * 若{@code parentKey}或{@code childKey}对应节点都存在,则移除指定该父节点与其直接关联的指定子节点间的引用关系 + * + * @param parentKey 父节点的key + * @param childKey 子节点 + */ + void unlinkNode(K parentKey, K childKey); + + // ===================== 父节点相关方法 ===================== + + /** + * 获取指定节点所在树结构的全部树节点
+ * 比如:存在 a -> b -> c 的关系,则输入 a/b/c 都将返回 a, b, c + * + * @param key 指定节点的key + * @return 节点 + */ + default Set> getTreeNodes(K key) { + final TreeEntry target = get(key); + if (ObjectUtil.isNull(target)) { + return Collections.emptySet(); + } + final Set> results = CollUtil.newLinkedHashSet(target.getRoot()); + CollUtil.addAll(results, target.getRoot().getChildren().values()); + return results; + } + + /** + * 获取以指定节点作为叶子节点的树结构,然后获取该树结构的根节点
+ * 比如:存在 a -> b -> c 的关系,则输入 a/b/c 都将返回 a + * + * @param key 指定节点的key + * @return 节点 + */ + default TreeEntry getRootNode(K key) { + return Opt.ofNullable(get(key)) + .map(TreeEntry::getRoot) + .orElse(null); + } + + /** + * 获取指定节点的直接父节点
+ * 比如:若存在 a -> b -> c 的关系,此时输入 a 将返回 null,输入 b 将返回 a,输入 c 将返回 b + * + * @param key 指定节点的key + * @return 节点 + */ + default TreeEntry getDeclaredParentNode(K key) { + return Opt.ofNullable(get(key)) + .map(TreeEntry::getDeclaredParent) + .orElse(null); + } + + /** + * 获取以指定节点作为叶子节点的树结构,然后获取该树结构中指定节点的指定父节点 + * + * @param key 指定节点的key + * @param parentKey 指定父节点key + * @return 节点 + */ + default TreeEntry getParentNode(K key, K parentKey) { + return Opt.ofNullable(get(key)) + .map(t -> t.getParent(parentKey)) + .orElse(null); + } + + /** + * 获取以指定节点作为叶子节点的树结构,然后确认该树结构中当前节点是否存在指定父节点 + * + * @param key 指定节点的key + * @param parentKey 指定父节点的key + * @return 是否 + */ + default boolean containsParentNode(K key, K parentKey) { + return Opt.ofNullable(get(key)) + .map(m -> m.containsParent(parentKey)) + .orElse(false); + } + + /** + * 获取指定节点的值 + * + * @param key 节点的key + * @return 节点值,若节点不存在,或节点值为null都将返回null + */ + default V getNodeValue(K key) { + return Opt.ofNullable(get(key)) + .map(TreeEntry::getValue) + .get(); + } + + // ===================== 子节点相关方法 ===================== + + /** + * 判断以该父节点作为根节点的树结构中是否具有指定子节点 + * + * @param parentKey 父节点 + * @param childKey 子节点 + * @return 是否 + */ + default boolean containsChildNode(K parentKey, K childKey) { + return Opt.ofNullable(get(parentKey)) + .map(m -> m.containsChild(childKey)) + .orElse(false); + } + + /** + * 获取指定父节点直接关联的子节点
+ * 比如:若存在 a -> b -> c 的关系,此时输入 b 将返回 c,输入 a 将返回 b + * + * @param key key + * @return 节点 + */ + default Collection> getDeclaredChildNodes(K key) { + return Opt.ofNullable(get(key)) + .map(TreeEntry::getDeclaredChildren) + .map(Map::values) + .orElseGet(Collections::emptyList); + } + + /** + * 获取指定父节点的全部子节点
+ * 比如:若存在 a -> b -> c 的关系,此时输入 b 将返回 c,输入 a 将返回 b,c + * + * @param key key + * @return 该节点的全部子节点 + */ + default Collection> getChildNodes(K key) { + return Opt.ofNullable(get(key)) + .map(TreeEntry::getChildren) + .map(Map::values) + .orElseGet(Collections::emptyList); + } + +} diff --git a/src/main/java/cn/hutool/core/map/FuncKeyMap.java b/src/main/java/cn/hutool/core/map/FuncKeyMap.java new file mode 100644 index 0000000..91a6f23 --- /dev/null +++ b/src/main/java/cn/hutool/core/map/FuncKeyMap.java @@ -0,0 +1,48 @@ +package cn.hutool.core.map; + +import java.util.Map; +import java.util.function.Function; + +/** + * 自定义函数Key风格的Map + * + * @param 键类型 + * @param 值类型 + * @author Looly + * @since 5.6.0 + */ +public class FuncKeyMap extends CustomKeyMap { + private static final long serialVersionUID = 1L; + + private final Function keyFunc; + + // ------------------------------------------------------------------------- Constructor start + + /** + * 构造
+ * 注意提供的Map中不能有键值对,否则可能导致自定义key失效 + * + * @param emptyMap Map,提供的空map + * @param keyFunc 自定义KEY的函数 + */ + public FuncKeyMap(Map emptyMap, Function keyFunc) { + super(emptyMap); + this.keyFunc = keyFunc; + } + // ------------------------------------------------------------------------- Constructor end + + /** + * 根据函数自定义键 + * + * @param key KEY + * @return 驼峰Key + */ + @Override + protected K customKey(Object key) { + if (null != this.keyFunc) { + return keyFunc.apply(key); + } + //noinspection unchecked + return (K)key; + } +} diff --git a/src/main/java/cn/hutool/core/map/FuncMap.java b/src/main/java/cn/hutool/core/map/FuncMap.java new file mode 100644 index 0000000..7007a28 --- /dev/null +++ b/src/main/java/cn/hutool/core/map/FuncMap.java @@ -0,0 +1,73 @@ +package cn.hutool.core.map; + +import java.util.Map; +import java.util.function.Function; +import java.util.function.Supplier; + +/** + * 自定义键值函数风格的Map + * + * @param 键类型 + * @param 值类型 + * @author Looly + * @since 5.8.0 + */ +public class FuncMap extends TransMap { + private static final long serialVersionUID = 1L; + + private final Function keyFunc; + private final Function valueFunc; + + // ------------------------------------------------------------------------- Constructor start + + /** + * 构造
+ * 注意提供的Map中不能有键值对,否则可能导致自定义key失效 + * + * @param mapFactory Map,提供的空map + * @param keyFunc 自定义KEY的函数 + * @param valueFunc 自定义value函数 + */ + public FuncMap(Supplier> mapFactory, Function keyFunc, Function valueFunc) { + this(mapFactory.get(), keyFunc, valueFunc); + } + + /** + * 构造
+ * 注意提供的Map中不能有键值对,否则可能导致自定义key失效 + * + * @param emptyMap Map,提供的空map + * @param keyFunc 自定义KEY的函数 + * @param valueFunc 自定义value函数 + */ + public FuncMap(Map emptyMap, Function keyFunc, Function valueFunc) { + super(emptyMap); + this.keyFunc = keyFunc; + this.valueFunc = valueFunc; + } + // ------------------------------------------------------------------------- Constructor end + + /** + * 根据函数自定义键 + * + * @param key KEY + * @return 驼峰Key + */ + @Override + protected K customKey(Object key) { + if (null != this.keyFunc) { + return keyFunc.apply(key); + } + //noinspection unchecked + return (K) key; + } + + @Override + protected V customValue(Object value) { + if (null != this.valueFunc) { + return valueFunc.apply(value); + } + //noinspection unchecked + return (V) value; + } +} diff --git a/src/main/java/cn/hutool/core/map/LinkedForestMap.java b/src/main/java/cn/hutool/core/map/LinkedForestMap.java new file mode 100644 index 0000000..6ddbb54 --- /dev/null +++ b/src/main/java/cn/hutool/core/map/LinkedForestMap.java @@ -0,0 +1,736 @@ +package cn.hutool.core.map; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.ClassUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; + +import java.util.*; +import java.util.function.BiConsumer; +import java.util.function.BiPredicate; +import java.util.function.Consumer; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +/** + * {@link ForestMap}的基本实现。 + * + *

该集合可以被视为以{@link TreeEntryNode#getKey()}作为key,{@link TreeEntryNode}实例作为value的{@link LinkedHashMap}。
+ * 使用时,将每一对键与值对视为一个{@link TreeEntryNode}节点,节点的id即为{@link TreeEntryNode#getKey()}, + * 任何情况下使用相同的key都将会访问到同一个节点。
+ * + *

节点通过key形成父子关系,并最终构成多叉树结构,多组平行的多叉树将在当前集合中构成森林。 + * 使用者可以通过{@link ForestMap}本身的方法来对森林进行操作或访问, + * 也可以在获取到{@link TreeEntry}后,使用节点本身的方法对数进行操作或访问。 + * + * @param key类型 + * @author huangchengxing + */ +public class LinkedForestMap implements ForestMap { + + /** + * 节点集合 + */ + private final Map> nodes; + + /** + * 当指定节点已经与其他节点构成了父子关系,是否允许将该节点的父节点强制替换为指定节点 + */ + private final boolean allowOverrideParent; + + /** + * 构建{@link LinkedForestMap} + * + * @param allowOverrideParent 当指定节点已经与其他节点构成了父子关系,是否允许将该节点的父节点强制替换为指定节点 + */ + public LinkedForestMap(boolean allowOverrideParent) { + this.allowOverrideParent = allowOverrideParent; + this.nodes = new LinkedHashMap<>(); + } + + // ====================== Map接口实现 ====================== + + /** + * 获取当前实例中的节点个数 + * + * @return 节点个数 + */ + @Override + public int size() { + return nodes.size(); + } + + /** + * 当前实例是否为空 + * + * @return 是否 + */ + @Override + public boolean isEmpty() { + return nodes.isEmpty(); + } + + /** + * 当前实例中是否存在key对应的节点 + * + * @param key key + * @return 是否 + */ + @Override + public boolean containsKey(Object key) { + return nodes.containsKey(key); + } + + /** + * 当前实例中是否存在对应的{@link TreeEntry}实例 + * + * @param value {@link TreeEntry}实例 + * @return 是否 + */ + @Override + public boolean containsValue(Object value) { + return nodes.containsValue(value); + } + + /** + * 获取key对应的节点 + * + * @param key key + * @return 节点 + */ + @Override + public TreeEntry get(Object key) { + return nodes.get(key); + } + + /** + * 将指定节点从当前{@link Map}中删除 + *

    + *
  • 若存在父节点或子节点,则将其断开其与父节点或子节点的引用关系;
  • + *
  • + * 若同时存在父节点或子节点,则会在删除后将让子节点直接成为父节点的子节点,比如:
    + * 现有引用关系 a -> b -> c,删除 b 后,将有 a -> c + *
  • + *
+ * + * @param key 节点的key + * @return 删除的且引用关系已经改变的节点,若key没有对应节点,则返回null + */ + @Override + public TreeEntry remove(Object key) { + final TreeEntryNode target = nodes.remove(key); + if (ObjectUtil.isNull(target)) { + return null; + } + // 若存在父节点: + // 1.将该目标从父节点的子节点中移除 + // 2.将目标的子节点直接将目标的父节点作为父节点 + if (target.hasParent()) { + final TreeEntryNode parent = target.getDeclaredParent(); + final Map> targetChildren = target.getChildren(); + parent.removeDeclaredChild(target.getKey()); + target.clear(); + targetChildren.forEach((k, c) -> parent.addChild((TreeEntryNode) c)); + } + return target; + } + + /** + * 将当前集合清空,并清除全部节点间的引用关系 + */ + @Override + public void clear() { + nodes.values().forEach(TreeEntryNode::clear); + nodes.clear(); + } + + /** + * 返回当前实例中全部的key组成的{@link Set}集合 + * + * @return 集合 + */ + @Override + public Set keySet() { + return nodes.keySet(); + } + + /** + * 返回当前实例中全部{@link TreeEntry}组成的{@link Collection}集合 + * + * @return 集合 + */ + @Override + public Collection> values() { + return new ArrayList<>(nodes.values()); + } + + /** + * 由key与{@link TreeEntry}组成的键值对实体的{@link Set}集合。 + * 注意,返回集合中{@link Entry#setValue(Object)}不支持调用。 + * + * @return 集合 + */ + @Override + public Set>> entrySet() { + return nodes.entrySet().stream() + .map(this::wrap) + .collect(Collectors.toSet()); + } + + /** + * 将{@link TreeEntryNode}包装为{@link EntryNodeWrapper} + */ + private Entry> wrap(Entry> nodeEntry) { + return new EntryNodeWrapper<>(nodeEntry.getValue()); + } + + // ====================== ForestMap接口实现 ====================== + + /** + * 添加一个节点 + *
    + *
  • 若key对应节点不存在,则以传入的键值创建一个新的节点;
  • + *
  • 若key对应节点存在,则将该节点的值替换为{@code node}指定的值;
  • + *
+ * + * @param key 节点的key + * @param value 节点的value + * @return 节点,若key已有对应节点,则返回具有旧值的节点,否则返回null + */ + @Override + public TreeEntryNode putNode(K key, V value) { + TreeEntryNode target = nodes.get(key); + if (ObjectUtil.isNotNull(target)) { + final V oldVal = target.getValue(); + target.setValue(value); + return target.copy(oldVal); + } + target = new TreeEntryNode<>(null, key, value); + nodes.put(key, target); + return null; + } + + /** + * 同时添加父子节点: + *
    + *
  • 若{@code parentKey}或{@code childKey}对应的节点不存在,则会根据键值创建一个对应的节点;
  • + *
  • 若{@code parentKey}或{@code childKey}对应的节点存在,则会更新对应节点的值;
  • + *
+ * 该操作等同于: + *
+	 *     TreeEntry<K, V>  parent = putNode(parentKey, parentValue);
+	 *     TreeEntry<K, V>  child = putNode(childKey, childValue);
+	 *     linkNodes(parentKey, childKey);
+	 * 
+ * + * @param parentKey 父节点的key + * @param parentValue 父节点的value + * @param childKey 子节点的key + * @param childValue 子节点的值 + */ + @Override + public void putLinkedNodes(K parentKey, V parentValue, K childKey, V childValue) { + linkNodes(parentKey, childKey, (parent, child) -> { + parent.setValue(parentValue); + child.setValue(childValue); + }); + } + + /** + * 添加子节点,并为子节点指定父节点: + *
    + *
  • 若{@code parentKey}或{@code childKey}对应的节点不存在,则会根据键值创建一个对应的节点;
  • + *
  • 若{@code parentKey}或{@code childKey}对应的节点存在,则会更新对应节点的值;
  • + *
+ * + * @param parentKey 父节点的key + * @param childKey 子节点的key + * @param childValue 子节点的值 + */ + @Override + public void putLinkedNodes(K parentKey, K childKey, V childValue) { + linkNodes(parentKey, childKey, (parent, child) -> child.setValue(childValue)); + } + + /** + * 为指定的节点建立父子关系,若{@code parentKey}或{@code childKey}对应节点不存在,则会创建一个对应的值为null的空节点 + * + * @param parentKey 父节点的key + * @param childKey 子节点的key + * @param consumer 对父节点和子节点的操作,允许为null + */ + @Override + public void linkNodes(K parentKey, K childKey, BiConsumer, TreeEntry> consumer) { + consumer = ObjectUtil.defaultIfNull(consumer, (parent, child) -> { + }); + final TreeEntryNode parentNode = nodes.computeIfAbsent(parentKey, t -> new TreeEntryNode<>(null, t)); + TreeEntryNode childNode = nodes.get(childKey); + + // 1.子节点不存在 + if (ObjectUtil.isNull(childNode)) { + childNode = new TreeEntryNode<>(parentNode, childKey); + consumer.accept(parentNode, childNode); + nodes.put(childKey, childNode); + return; + } + + // 2.子节点存在,且已经是该父节点的子节点了 + if (ObjectUtil.equals(parentNode, childNode.getDeclaredParent())) { + consumer.accept(parentNode, childNode); + return; + } + + // 3.子节点存在,但是未与其他节点构成父子关系 + if (!childNode.hasParent()) { + parentNode.addChild(childNode); + } + // 4.子节点存在,且已经与其他节点构成父子关系,但是允许子节点直接修改其父节点 + else if (allowOverrideParent) { + childNode.getDeclaredParent().removeDeclaredChild(childNode.getKey()); + parentNode.addChild(childNode); + } + // 5.子节点存在,且已经与其他节点构成父子关系,但是不允许子节点直接修改其父节点 + else { + throw new IllegalArgumentException(StrUtil.format( + "[{}] has been used as child of [{}], can not be overwrite as child of [{}]", + childNode.getKey(), childNode.getDeclaredParent().getKey(), parentKey + )); + } + consumer.accept(parentNode, childNode); + } + + /** + * 移除指定父节点与其直接关联的子节点间的引用关系,但是不会将该节点从集合中删除 + * + * @param parentKey 父节点的key + * @param childKey 子节点 + */ + @Override + public void unlinkNode(K parentKey, K childKey) { + final TreeEntryNode childNode = nodes.get(childKey); + if (ObjectUtil.isNull(childNode)) { + return; + } + if (childNode.hasParent()) { + childNode.getDeclaredParent().removeDeclaredChild(childNode.getKey()); + } + } + + /** + * 树节点 + * + * @param key类型 + * @author huangchengxing + */ + public static class TreeEntryNode implements TreeEntry { + + /** + * 根节点 + */ + private TreeEntryNode root; + + /** + * 父节点 + */ + private TreeEntryNode parent; + + /** + * 权重,表示到根节点的距离 + */ + private int weight; + + /** + * 子节点 + */ + private final Map> children; + + /** + * key + */ + private final K key; + + /** + * 值 + */ + private V value; + + /** + * 创建一个节点 + * + * @param parent 节点的父节点 + * @param key 节点的key + */ + public TreeEntryNode(TreeEntryNode parent, K key) { + this(parent, key, null); + } + + /** + * 创建一个节点 + * + * @param parent 节点的父节点 + * @param key 节点的key + * @param value 节点的value + */ + public TreeEntryNode(TreeEntryNode parent, K key, V value) { + this.parent = parent; + this.key = key; + this.value = value; + this.children = new LinkedHashMap<>(); + if (ObjectUtil.isNull(parent)) { + this.root = this; + this.weight = 0; + } else { + parent.addChild(this); + this.weight = parent.weight + 1; + this.root = parent.root; + } + } + + /** + * 获取当前节点的key + * + * @return 节点的key + */ + @Override + public K getKey() { + return key; + } + + /** + * 获取当前节点与根节点的距离 + * + * @return 当前节点与根节点的距离 + */ + @Override + public int getWeight() { + return weight; + } + + /** + * 获取节点的value + * + * @return 节点的value + */ + @Override + public V getValue() { + return value; + } + + /** + * 设置节点的value + * + * @param value 节点的value + * @return 节点的旧value + */ + @Override + public V setValue(V value) { + final V oldVal = getValue(); + this.value = value; + return oldVal; + } + + // ================== 父节点的操作 ================== + + /** + * 从当前节点开始,向上递归当前节点的父节点 + * + * @param includeCurrent 是否处理当前节点 + * @param consumer 对节点的操作 + * @param breakTraverse 是否终止遍历 + * @return 遍历到的最后一个节点 + */ + TreeEntryNode traverseParentNodes( + boolean includeCurrent, Consumer> consumer, Predicate> breakTraverse) { + breakTraverse = ObjectUtil.defaultIfNull(breakTraverse, a -> n -> false); + TreeEntryNode curr = includeCurrent ? this : this.parent; + while (ObjectUtil.isNotNull(curr)) { + consumer.accept(curr); + if (breakTraverse.test(curr)) { + break; + } + curr = curr.parent; + } + return curr; + } + + /** + * 当前节点是否为根节点 + * + * @return 当前节点是否为根节点 + */ + public boolean isRoot() { + return getRoot() == this; + } + + /** + * 获取以当前节点作为叶子节点的树结构,然后获取该树结构的根节点 + * + * @return 根节点 + */ + @Override + public TreeEntryNode getRoot() { + if (ObjectUtil.isNotNull(this.root)) { + return this.root; + } else { + this.root = traverseParentNodes(true, p -> { + }, p -> !p.hasParent()); + } + return this.root; + } + + /** + * 获取当前节点直接关联的父节点 + * + * @return 父节点,当节点不存在对应父节点时返回null + */ + @Override + public TreeEntryNode getDeclaredParent() { + return parent; + } + + /** + * 获取以当前节点作为叶子节点的树结构,然后获取该树结构中当前节点的指定父节点 + * + * @param key 指定父节点的key + * @return 指定父节点,当不存在时返回null + */ + @Override + public TreeEntryNode getParent(K key) { + return traverseParentNodes(false, p -> { + }, p -> p.equalsKey(key)); + } + + /** + * 获取以当前节点作为根节点的树结构,然后遍历所有节点 + * + * @param includeSelf 是否处理当前节点 + * @param nodeConsumer 对节点的处理 + */ + @Override + public void forEachChild(boolean includeSelf, Consumer> nodeConsumer) { + traverseChildNodes(includeSelf, (index, child) -> nodeConsumer.accept(child), null); + } + + /** + * 指定key与当前节点的key是否相等 + * + * @param key 要比较的key + * @return 是否key一致 + */ + public boolean equalsKey(K key) { + return ObjectUtil.equal(getKey(), key); + } + + // ================== 子节点的操作 ================== + + /** + * 从当前节点开始,按广度优先向下遍历当前节点的所有子节点 + * + * @param includeCurrent 是否包含当前节点 + * @param consumer 对节点与节点和当前节点的距离的操作,当{code includeCurrent}为false时下标从1开始,否则从0开始 + * @param breakTraverse 是否终止遍历,为null时默认总是返回{@code true} + * @return 遍历到的最后一个节点 + */ + TreeEntryNode traverseChildNodes( + boolean includeCurrent, BiConsumer> consumer, BiPredicate> breakTraverse) { + breakTraverse = ObjectUtil.defaultIfNull(breakTraverse, (i, n) -> false); + final Deque>> keyNodeDeque = CollUtil.newLinkedList(CollUtil.newArrayList(this)); + boolean needProcess = includeCurrent; + int index = includeCurrent ? 0 : 1; + TreeEntryNode lastNode = null; + while (!keyNodeDeque.isEmpty()) { + final List> curr = keyNodeDeque.removeFirst(); + final List> next = new ArrayList<>(); + for (final TreeEntryNode node : curr) { + if (needProcess) { + consumer.accept(index, node); + if (breakTraverse.test(index, node)) { + return node; + } + } else { + needProcess = true; + } + CollUtil.addAll(next, node.children.values()); + } + if (!next.isEmpty()) { + keyNodeDeque.addLast(next); + } + lastNode = CollUtil.getLast(next); + index++; + } + return lastNode; + } + + + /** + * 添加子节点 + * + * @param child 子节点 + * @throws IllegalArgumentException 当要添加的子节点已经是其自身父节点时抛出 + */ + void addChild(TreeEntryNode child) { + if (containsChild(child.key)) { + return; + } + + // 检查循环引用 + traverseParentNodes(true, s -> Assert.notEquals( + s.key, child.key, + "circular reference between [{}] and [{}]!", + s.key, this.key + ), null); + + // 调整该节点的信息 + child.parent = this; + child.traverseChildNodes(true, (i, c) -> { + c.root = getRoot(); + c.weight = i + getWeight() + 1; + }, null); + + // 将该节点添加为当前节点的子节点 + children.put(child.key, child); + } + + /** + * 移除子节点 + * + * @param key 子节点 + */ + void removeDeclaredChild(K key) { + final TreeEntryNode child = children.get(key); + if (ObjectUtil.isNull(child)) { + return; + } + + // 断开该节点与其父节点的关系 + this.children.remove(key); + + // 重置子节点及其下属节点的相关属性 + child.parent = null; + child.traverseChildNodes(true, (i, c) -> { + c.root = child; + c.weight = i; + }, null); + } + + /** + * 获取以当前节点作为根节点的树结构,然后获取该树结构中的当前节点的指定子节点 + * + * @param key 指定子节点的key + * @return 节点 + */ + @Override + public TreeEntryNode getChild(K key) { + return traverseChildNodes(false, (i, c) -> { + }, (i, c) -> c.equalsKey(key)); + } + + /** + * 获取当前节点直接关联的子节点 + * + * @return 节点 + */ + @Override + public Map> getDeclaredChildren() { + return new LinkedHashMap<>(this.children); + } + + /** + * 获取以当前节点作为根节点的树结构,然后按广度优先获取该树结构中的当前节点的全部子节点 + * + * @return 节点 + */ + @Override + public Map> getChildren() { + final Map> childrenMap = new LinkedHashMap<>(); + traverseChildNodes(false, (i, c) -> childrenMap.put(c.getKey(), c), null); + return childrenMap; + } + + /** + * 移除对子节点、父节点与根节点的全部引用 + */ + void clear() { + this.root = null; + this.children.clear(); + this.parent = null; + } + + /** + * 比较目标对象与当前{@link TreeEntry}是否相等。
+ * 默认只要{@link TreeEntry#getKey()}的返回值相同,即认为两者相等 + * + * @param o 目标对象 + * @return 是否 + */ + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || this.getClass().equals(o.getClass()) || ClassUtil.isAssignable(this.getClass(), o.getClass())) { + return false; + } + final TreeEntry treeEntry = (TreeEntry) o; + return ObjectUtil.equals(this.getKey(), treeEntry.getKey()); + } + + /** + * 返回当前{@link TreeEntry}的哈希值。
+ * 默认总是返回{@link TreeEntry#getKey()}的哈希值 + * + * @return 哈希值 + */ + @Override + public int hashCode() { + return Objects.hash(getKey()); + } + + /** + * 复制一个当前节点 + * + * @param value 复制的节点的值 + * @return 节点 + */ + TreeEntryNode copy(V value) { + TreeEntryNode copiedNode = new TreeEntryNode<>(this.parent, this.key, ObjectUtil.defaultIfNull(value, this.value)); + copiedNode.children.putAll(children); + return copiedNode; + } + + } + + /** + * {@link Entry}包装类 + * + * @param key类型 + * @param value类型 + * @param 包装的{@link TreeEntry}类型 + * @see #entrySet() + * @see #values() + */ + public static class EntryNodeWrapper> implements Entry> { + private final N entryNode; + + EntryNodeWrapper(N entryNode) { + this.entryNode = entryNode; + } + + @Override + public K getKey() { + return entryNode.getKey(); + } + + @Override + public TreeEntry getValue() { + return entryNode; + } + + @Override + public TreeEntry setValue(TreeEntry value) { + throw new UnsupportedOperationException(); + } + } + +} diff --git a/src/main/java/cn/hutool/core/map/MapBuilder.java b/src/main/java/cn/hutool/core/map/MapBuilder.java new file mode 100644 index 0000000..2f605d7 --- /dev/null +++ b/src/main/java/cn/hutool/core/map/MapBuilder.java @@ -0,0 +1,188 @@ +package cn.hutool.core.map; + + +import cn.hutool.core.builder.Builder; + +import java.util.Map; +import java.util.function.Supplier; + +/** + * Map创建类 + * + * @param Key类型 + * @param Value类型 + * @since 3.1.1 + */ +public class MapBuilder implements Builder> { + private static final long serialVersionUID = 1L; + + private final Map map; + + /** + * 创建Builder,默认HashMap实现 + * + * @param Key类型 + * @param Value类型 + * @return MapBuilder + * @since 5.3.0 + */ + public static MapBuilder create() { + return create(false); + } + + /** + * 创建Builder + * + * @param Key类型 + * @param Value类型 + * @param isLinked true创建LinkedHashMap,false创建HashMap + * @return MapBuilder + * @since 5.3.0 + */ + public static MapBuilder create(boolean isLinked) { + return create(MapUtil.newHashMap(isLinked)); + } + + /** + * 创建Builder + * + * @param Key类型 + * @param Value类型 + * @param map Map实体类 + * @return MapBuilder + * @since 3.2.3 + */ + public static MapBuilder create(Map map) { + return new MapBuilder<>(map); + } + + /** + * 链式Map创建类 + * + * @param map 要使用的Map实现类 + */ + public MapBuilder(Map map) { + this.map = map; + } + + /** + * 链式Map创建 + * + * @param k Key类型 + * @param v Value类型 + * @return 当前类 + */ + public MapBuilder put(K k, V v) { + map.put(k, v); + return this; + } + + /** + * 链式Map创建 + * + * @param condition put条件 + * @param k Key类型 + * @param v Value类型 + * @return 当前类 + * @since 5.7.5 + */ + public MapBuilder put(boolean condition, K k, V v) { + if (condition) { + put(k, v); + } + return this; + } + + /** + * 链式Map创建 + * + * @param condition put条件 + * @param k Key类型 + * @param supplier Value类型结果提供方 + * @return 当前类 + * @since 5.7.5 + */ + public MapBuilder put(boolean condition, K k, Supplier supplier) { + if (condition) { + put(k, supplier.get()); + } + return this; + } + + /** + * 链式Map创建 + * + * @param map 合并map + * @return 当前类 + */ + public MapBuilder putAll(Map map) { + this.map.putAll(map); + return this; + } + + /** + * 清空Map + * + * @return this + * @since 5.7.23 + */ + public MapBuilder clear() { + this.map.clear(); + return this; + } + + /** + * 创建后的map + * + * @return 创建后的map + */ + public Map map() { + return map; + } + + /** + * 创建后的map + * + * @return 创建后的map + * @since 3.3.0 + */ + @Override + public Map build() { + return map(); + } + + /** + * 将map转成字符串 + * + * @param separator entry之间的连接符 + * @param keyValueSeparator kv之间的连接符 + * @return 连接字符串 + */ + public String join(String separator, final String keyValueSeparator) { + return MapUtil.join(this.map, separator, keyValueSeparator); + } + + /** + * 将map转成字符串 + * + * @param separator entry之间的连接符 + * @param keyValueSeparator kv之间的连接符 + * @return 连接后的字符串 + */ + public String joinIgnoreNull(String separator, final String keyValueSeparator) { + return MapUtil.joinIgnoreNull(this.map, separator, keyValueSeparator); + } + + /** + * 将map转成字符串 + * + * @param separator entry之间的连接符 + * @param keyValueSeparator kv之间的连接符 + * @param isIgnoreNull 是否忽略null的键和值 + * @return 连接后的字符串 + */ + public String join(String separator, final String keyValueSeparator, boolean isIgnoreNull) { + return MapUtil.join(this.map, separator, keyValueSeparator, isIgnoreNull); + } + +} diff --git a/src/main/java/cn/hutool/core/map/MapProxy.java b/src/main/java/cn/hutool/core/map/MapProxy.java new file mode 100644 index 0000000..6efa792 --- /dev/null +++ b/src/main/java/cn/hutool/core/map/MapProxy.java @@ -0,0 +1,183 @@ +package cn.hutool.core.map; + +import cn.hutool.core.convert.Convert; +import cn.hutool.core.getter.OptNullBasicTypeFromObjectGetter; +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.BooleanUtil; +import cn.hutool.core.util.ClassLoaderUtil; +import cn.hutool.core.util.StrUtil; + +import java.io.Serializable; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.util.Collection; +import java.util.Map; +import java.util.Set; + +/** + * Map代理,提供各种getXXX方法,并提供默认值支持 + * + * @author looly + * @since 3.2.0 + */ +public class MapProxy implements Map, OptNullBasicTypeFromObjectGetter, InvocationHandler, Serializable { + private static final long serialVersionUID = 1L; + + @SuppressWarnings("rawtypes") + Map map; + + /** + * 创建代理Map
+ * 此类对Map做一次包装,提供各种getXXX方法 + * + * @param map 被代理的Map + * @return {@link MapProxy} + */ + public static MapProxy create(Map map) { + return (map instanceof MapProxy) ? (MapProxy) map : new MapProxy(map); + } + + /** + * 构造 + * + * @param map 被代理的Map + */ + public MapProxy(Map map) { + this.map = map; + } + + @Override + public Object getObj(Object key, Object defaultValue) { + final Object value = map.get(key); + return null != value ? value : defaultValue; + } + + @Override + public int size() { + return map.size(); + } + + @Override + public boolean isEmpty() { + return map.isEmpty(); + } + + @Override + public boolean containsKey(Object key) { + return map.containsKey(key); + } + + @Override + public boolean containsValue(Object value) { + return map.containsValue(value); + } + + @Override + public Object get(Object key) { + return map.get(key); + } + + @SuppressWarnings("unchecked") + @Override + public Object put(Object key, Object value) { + return map.put(key, value); + } + + @Override + public Object remove(Object key) { + return map.remove(key); + } + + @SuppressWarnings({"unchecked", "NullableProblems"}) + @Override + public void putAll(Map m) { + map.putAll(m); + } + + @Override + public void clear() { + map.clear(); + } + + @SuppressWarnings({"unchecked", "NullableProblems"}) + @Override + public Set keySet() { + return map.keySet(); + } + + @SuppressWarnings({"unchecked", "NullableProblems"}) + @Override + public Collection values() { + return map.values(); + } + + @SuppressWarnings({"unchecked", "NullableProblems"}) + @Override + public Set> entrySet() { + return map.entrySet(); + } + + @Override + public Object invoke(Object proxy, Method method, Object[] args) { + final Class[] parameterTypes = method.getParameterTypes(); + if (ArrayUtil.isEmpty(parameterTypes)) { + final Class returnType = method.getReturnType(); + if (void.class != returnType) { + // 匹配Getter + final String methodName = method.getName(); + String fieldName = null; + if (methodName.startsWith("get")) { + // 匹配getXXX + fieldName = StrUtil.removePreAndLowerFirst(methodName, 3); + } else if (BooleanUtil.isBoolean(returnType) && methodName.startsWith("is")) { + // 匹配isXXX + fieldName = StrUtil.removePreAndLowerFirst(methodName, 2); + }else if ("hashCode".equals(methodName)) { + return this.hashCode(); + } else if ("toString".equals(methodName)) { + return this.toString(); + } + + if (StrUtil.isNotBlank(fieldName)) { + if (!this.containsKey(fieldName)) { + // 驼峰不存在转下划线尝试 + fieldName = StrUtil.toUnderlineCase(fieldName); + } + return Convert.convert(method.getGenericReturnType(), this.get(fieldName)); + } + } + + } else if (1 == parameterTypes.length) { + // 匹配Setter + final String methodName = method.getName(); + if (methodName.startsWith("set")) { + final String fieldName = StrUtil.removePreAndLowerFirst(methodName, 3); + if (StrUtil.isNotBlank(fieldName)) { + this.put(fieldName, args[0]); + final Class returnType = method.getReturnType(); + if(returnType.isInstance(proxy)){ + return proxy; + } + } + } else if ("equals".equals(methodName)) { + return this.equals(args[0]); + } + } + + throw new UnsupportedOperationException(method.toGenericString()); + } + + /** + * 将Map代理为指定接口的动态代理对象 + * + * @param 代理的Bean类型 + * @param interfaceClass 接口 + * @return 代理对象 + * @since 4.5.2 + */ + @SuppressWarnings("unchecked") + public T toProxyBean(Class interfaceClass) { + return (T) Proxy.newProxyInstance(ClassLoaderUtil.getClassLoader(), new Class[]{interfaceClass}, this); + } +} diff --git a/src/main/java/cn/hutool/core/map/MapUtil.java b/src/main/java/cn/hutool/core/map/MapUtil.java new file mode 100644 index 0000000..68f0c64 --- /dev/null +++ b/src/main/java/cn/hutool/core/map/MapUtil.java @@ -0,0 +1,1488 @@ +package cn.hutool.core.map; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.convert.Convert; +import cn.hutool.core.exceptions.UtilException; +import cn.hutool.core.lang.Editor; +import cn.hutool.core.lang.Filter; +import cn.hutool.core.lang.Pair; +import cn.hutool.core.lang.TypeReference; +import cn.hutool.core.stream.CollectorUtil; +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.ReflectUtil; +import cn.hutool.core.util.StrUtil; + +import java.util.AbstractMap; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.Date; +import java.util.HashMap; +import java.util.IdentityHashMap; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.NavigableMap; +import java.util.Set; +import java.util.SortedMap; +import java.util.TreeMap; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.BiFunction; +import java.util.function.Function; + +/** + * Map相关工具类 + * + * @author Looly + * @since 3.1.1 + */ +public class MapUtil { + + /** + * 默认初始大小 + */ + public static final int DEFAULT_INITIAL_CAPACITY = 16; + /** + * 默认增长因子,当Map的size达到 容量*增长因子时,开始扩充Map + */ + public static final float DEFAULT_LOAD_FACTOR = 0.75f; + + /** + * Map是否为空 + * + * @param map 集合 + * @return 是否为空 + */ + public static boolean isEmpty(Map map) { + return null == map || map.isEmpty(); + } + + /** + * Map是否为非空 + * + * @param map 集合 + * @return 是否为非空 + */ + public static boolean isNotEmpty(Map map) { + return null != map && !map.isEmpty(); + } + + /** + * 如果提供的集合为{@code null},返回一个不可变的默认空集合,否则返回原集合
+ * 空集合使用{@link Collections#emptyMap()} + * + * @param 键类型 + * @param 值类型 + * @param set 提供的集合,可能为null + * @return 原集合,若为null返回空集合 + * @since 4.6.3 + */ + public static Map emptyIfNull(Map set) { + return (null == set) ? Collections.emptyMap() : set; + } + + /** + * 如果给定Map为空,返回默认Map + * + * @param 集合类型 + * @param 键类型 + * @param 值类型 + * @param map Map + * @param defaultMap 默认Map + * @return 非空(empty)的原Map或默认Map + * @since 4.6.9 + */ + public static , K, V> T defaultIfEmpty(T map, T defaultMap) { + return isEmpty(map) ? defaultMap : map; + } + + // ----------------------------------------------------------------------------------------------- new HashMap + + /** + * 新建一个HashMap + * + * @param Key类型 + * @param Value类型 + * @return HashMap对象 + */ + public static HashMap newHashMap() { + return new HashMap<>(); + } + + /** + * 新建一个HashMap + * + * @param Key类型 + * @param Value类型 + * @param size 初始大小,由于默认负载因子0.75,传入的size会实际初始大小为size / 0.75 + 1 + * @param isLinked Map的Key是否有序,有序返回 {@link LinkedHashMap},否则返回 {@link HashMap} + * @return HashMap对象 + * @since 3.0.4 + */ + public static HashMap newHashMap(int size, boolean isLinked) { + final int initialCapacity = (int) (size / DEFAULT_LOAD_FACTOR) + 1; + return isLinked ? new LinkedHashMap<>(initialCapacity) : new HashMap<>(initialCapacity); + } + + /** + * 新建一个HashMap + * + * @param Key类型 + * @param Value类型 + * @param size 初始大小,由于默认负载因子0.75,传入的size会实际初始大小为size / 0.75 + 1 + * @return HashMap对象 + */ + public static HashMap newHashMap(int size) { + return newHashMap(size, false); + } + + /** + * 新建一个HashMap + * + * @param Key类型 + * @param Value类型 + * @param isLinked Map的Key是否有序,有序返回 {@link LinkedHashMap},否则返回 {@link HashMap} + * @return HashMap对象 + */ + public static HashMap newHashMap(boolean isLinked) { + return newHashMap(DEFAULT_INITIAL_CAPACITY, isLinked); + } + + /** + * 新建TreeMap,Key有序的Map + * + * @param key的类型 + * @param value的类型 + * @param comparator Key比较器 + * @return TreeMap + * @since 3.2.3 + */ + public static TreeMap newTreeMap(Comparator comparator) { + return new TreeMap<>(comparator); + } + + /** + * 新建TreeMap,Key有序的Map + * + * @param key的类型 + * @param value的类型 + * @param map Map + * @param comparator Key比较器 + * @return TreeMap + * @since 3.2.3 + */ + public static TreeMap newTreeMap(Map map, Comparator comparator) { + final TreeMap treeMap = new TreeMap<>(comparator); + if (!isEmpty(map)) { + treeMap.putAll(map); + } + return treeMap; + } + + /** + * 创建键不重复Map + * + * @param key的类型 + * @param value的类型 + * @param size 初始容量 + * @return {@link IdentityHashMap} + * @since 4.5.7 + */ + public static Map newIdentityMap(int size) { + return new IdentityHashMap<>(size); + } + + /** + * 新建一个初始容量为{@link MapUtil#DEFAULT_INITIAL_CAPACITY} 的ConcurrentHashMap + * + * @param key的类型 + * @param value的类型 + * @return ConcurrentHashMap + */ + public static ConcurrentHashMap newConcurrentHashMap() { + return new ConcurrentHashMap<>(DEFAULT_INITIAL_CAPACITY); + } + + /** + * 新建一个ConcurrentHashMap + * + * @param size 初始容量,当传入的容量小于等于0时,容量为{@link MapUtil#DEFAULT_INITIAL_CAPACITY} + * @param key的类型 + * @param value的类型 + * @return ConcurrentHashMap + */ + public static ConcurrentHashMap newConcurrentHashMap(int size) { + final int initCapacity = size <= 0 ? DEFAULT_INITIAL_CAPACITY : size; + return new ConcurrentHashMap<>(initCapacity); + } + + /** + * 传入一个Map将其转化为ConcurrentHashMap类型 + * + * @param map map + * @param key的类型 + * @param value的类型 + * @return ConcurrentHashMap + */ + public static ConcurrentHashMap newConcurrentHashMap(Map map) { + if (isEmpty(map)) { + return new ConcurrentHashMap<>(DEFAULT_INITIAL_CAPACITY); + } + return new ConcurrentHashMap<>(map); + } + + /** + * 创建Map
+ * 传入抽象Map{@link AbstractMap}和{@link Map}类将默认创建{@link HashMap} + * + * @param map键类型 + * @param map值类型 + * @param mapType map类型 + * @return {@link Map}实例 + */ + @SuppressWarnings("unchecked") + public static Map createMap(Class mapType) { + if (null == mapType || mapType.isAssignableFrom(AbstractMap.class)) { + return new HashMap<>(); + } else { + try { + return (Map) ReflectUtil.newInstance(mapType); + } catch (UtilException e) { + // 不支持的map类型,返回默认的HashMap + return new HashMap<>(); + } + } + } + + // ----------------------------------------------------------------------------------------------- value of + + /** + * 将单一键值对转换为Map + * + * @param 键类型 + * @param 值类型 + * @param key 键 + * @param value 值 + * @return {@link HashMap} + */ + public static HashMap of(K key, V value) { + return of(key, value, false); + } + + /** + * 将单一键值对转换为Map + * + * @param 键类型 + * @param 值类型 + * @param key 键 + * @param value 值 + * @param isOrder 是否有序 + * @return {@link HashMap} + */ + public static HashMap of(K key, V value, boolean isOrder) { + final HashMap map = newHashMap(isOrder); + map.put(key, value); + return map; + } + + /** + * 根据给定的Pair数组创建Map对象 + * + * @param 键类型 + * @param 值类型 + * @param pairs 键值对 + * @return Map + * @since 5.4.1 + * @deprecated 方法容易歧义,请使用 {@code #ofEntries(Entry[])} + */ + @SafeVarargs + @Deprecated + public static Map of(Pair... pairs) { + final Map map = new HashMap<>(); + for (Pair pair : pairs) { + map.put(pair.getKey(), pair.getValue()); + } + return map; + } + + /** + * 根据给定的Pair数组创建Map对象 + * + * @param 键类型 + * @param 值类型 + * @param entries 键值对 + * @return Map + * @see #entry(Object, Object) + * @since 5.8.0 + */ + @SafeVarargs + public static Map ofEntries(Entry... entries) { + final Map map = new HashMap<>(); + for (Entry pair : entries) { + map.put(pair.getKey(), pair.getValue()); + } + return map; + } + + /** + * 将数组转换为Map(HashMap),支持数组元素类型为: + * + *
+	 * Map.Entry
+	 * 长度大于1的数组(取前两个值),如果不满足跳过此元素
+	 * Iterable 长度也必须大于1(取前两个值),如果不满足跳过此元素
+	 * Iterator 长度也必须大于1(取前两个值),如果不满足跳过此元素
+	 * 
+ * + *
+	 * Map<Object, Object> colorMap = MapUtil.of(new String[][] {
+	 *    { "RED", "#FF0000" },
+	 *    { "GREEN", "#00FF00" },
+	 *    { "BLUE", "#0000FF" }
+	 * });
+	 * 
+ *

+ * 参考:commons-lang + * + * @param array 数组。元素类型为Map.Entry、数组、Iterable、Iterator + * @return {@link HashMap} + * @since 3.0.8 + */ + @SuppressWarnings("rawtypes") + public static HashMap of(Object[] array) { + if (array == null) { + return null; + } + final HashMap map = new HashMap<>((int) (array.length * 1.5)); + for (int i = 0; i < array.length; i++) { + final Object object = array[i]; + if (object instanceof Map.Entry) { + Entry entry = (Entry) object; + map.put(entry.getKey(), entry.getValue()); + } else if (object instanceof Object[]) { + final Object[] entry = (Object[]) object; + if (entry.length > 1) { + map.put(entry[0], entry[1]); + } + } else if (object instanceof Iterable) { + final Iterator iter = ((Iterable) object).iterator(); + if (iter.hasNext()) { + final Object key = iter.next(); + if (iter.hasNext()) { + final Object value = iter.next(); + map.put(key, value); + } + } + } else if (object instanceof Iterator) { + final Iterator iter = ((Iterator) object); + if (iter.hasNext()) { + final Object key = iter.next(); + if (iter.hasNext()) { + final Object value = iter.next(); + map.put(key, value); + } + } + } else { + throw new IllegalArgumentException(StrUtil.format("Array element {}, '{}', is not type of Map.Entry or Array or Iterable or Iterator", i, object)); + } + } + return map; + } + + /** + * 行转列,合并相同的键,值合并为列表
+ * 将Map列表中相同key的值组成列表做为Map的value
+ * 是{@link #toMapList(Map)}的逆方法
+ * 比如传入数据: + * + *

+	 * [
+	 *  {a: 1, b: 1, c: 1}
+	 *  {a: 2, b: 2}
+	 *  {a: 3, b: 3}
+	 *  {a: 4}
+	 * ]
+	 * 
+ *

+ * 结果是: + * + *

+	 * {
+	 *   a: [1,2,3,4]
+	 *   b: [1,2,3,]
+	 *   c: [1]
+	 * }
+	 * 
+ * + * @param 键类型 + * @param 值类型 + * @param mapList Map列表 + * @return Map + */ + public static Map> toListMap(Iterable> mapList) { + final HashMap> resultMap = new HashMap<>(); + if (CollUtil.isEmpty(mapList)) { + return resultMap; + } + + Set> entrySet; + for (Map map : mapList) { + entrySet = map.entrySet(); + K key; + List valueList; + for (Entry entry : entrySet) { + key = entry.getKey(); + valueList = resultMap.get(key); + if (null == valueList) { + valueList = CollUtil.newArrayList(entry.getValue()); + resultMap.put(key, valueList); + } else { + valueList.add(entry.getValue()); + } + } + } + + return resultMap; + } + + /** + * 列转行。将Map中值列表分别按照其位置与key组成新的map。
+ * 是{@link #toListMap(Iterable)}的逆方法
+ * 比如传入数据: + * + *
+	 * {
+	 *   a: [1,2,3,4]
+	 *   b: [1,2,3,]
+	 *   c: [1]
+	 * }
+	 * 
+ *

+ * 结果是: + * + *

+	 * [
+	 *  {a: 1, b: 1, c: 1}
+	 *  {a: 2, b: 2}
+	 *  {a: 3, b: 3}
+	 *  {a: 4}
+	 * ]
+	 * 
+ * + * @param 键类型 + * @param 值类型 + * @param listMap 列表Map + * @return Map列表 + */ + public static List> toMapList(Map> listMap) { + final List> resultList = new ArrayList<>(); + if (isEmpty(listMap)) { + return resultList; + } + + boolean isEnd;// 是否结束。标准是元素列表已耗尽 + int index = 0;// 值索引 + Map map; + do { + isEnd = true; + map = new HashMap<>(); + List vList; + int vListSize; + for (Entry> entry : listMap.entrySet()) { + vList = CollUtil.newArrayList(entry.getValue()); + vListSize = vList.size(); + if (index < vListSize) { + map.put(entry.getKey(), vList.get(index)); + if (index != vListSize - 1) { + // 当值列表中还有更多值(非最后一个),继续循环 + isEnd = false; + } + } + } + if (!map.isEmpty()) { + resultList.add(map); + } + index++; + } while (!isEnd); + + return resultList; + } + + /** + * 根据给定的entry列表,根据entry的key进行分组; + * + * @param 键类型 + * @param 值类型 + * @param entries entry列表 + * @return entries + */ + public static Map> grouping(Iterable> entries) { + final Map> map = new HashMap<>(); + if (CollUtil.isEmpty(entries)) { + return map; + } + for (final Entry pair : entries) { + final List values = map.computeIfAbsent(pair.getKey(), k -> new ArrayList<>()); + values.add(pair.getValue()); + } + return map; + } + + /** + * 将已知Map转换为key为驼峰风格的Map
+ * 如果KEY为非String类型,保留原值 + * + * @param key的类型 + * @param value的类型 + * @param map 原Map + * @return 驼峰风格Map + * @since 3.3.1 + */ + public static Map toCamelCaseMap(Map map) { + return (map instanceof LinkedHashMap) ? new CamelCaseLinkedMap<>(map) : new CamelCaseMap<>(map); + } + + /** + * 将键值对转换为二维数组,第一维是key,第二纬是value + * + * @param map map + * @return 数组 + * @since 4.1.9 + */ + public static Object[][] toObjectArray(Map map) { + if (map == null) { + return null; + } + final Object[][] result = new Object[map.size()][2]; + if (map.isEmpty()) { + return result; + } + int index = 0; + for (Entry entry : map.entrySet()) { + result[index][0] = entry.getKey(); + result[index][1] = entry.getValue(); + index++; + } + return result; + } + + // ----------------------------------------------------------------------------------------------- join + + /** + * 将map转成字符串 + * + * @param 键类型 + * @param 值类型 + * @param map Map + * @param separator entry之间的连接符 + * @param keyValueSeparator kv之间的连接符 + * @param otherParams 其它附加参数字符串(例如密钥) + * @return 连接字符串 + * @since 3.1.1 + */ + public static String join(Map map, String separator, String keyValueSeparator, String... otherParams) { + return join(map, separator, keyValueSeparator, false, otherParams); + } + + /** + * 根据参数排序后拼接为字符串,常用于签名 + * + * @param params 参数 + * @param separator entry之间的连接符 + * @param keyValueSeparator kv之间的连接符 + * @param isIgnoreNull 是否忽略null的键和值 + * @param otherParams 其它附加参数字符串(例如密钥) + * @return 签名字符串 + * @since 5.0.4 + */ + public static String sortJoin(Map params, String separator, String keyValueSeparator, boolean isIgnoreNull, + String... otherParams) { + return join(sort(params), separator, keyValueSeparator, isIgnoreNull, otherParams); + } + + /** + * 将map转成字符串,忽略null的键和值 + * + * @param 键类型 + * @param 值类型 + * @param map Map + * @param separator entry之间的连接符 + * @param keyValueSeparator kv之间的连接符 + * @param otherParams 其它附加参数字符串(例如密钥) + * @return 连接后的字符串 + * @since 3.1.1 + */ + public static String joinIgnoreNull(Map map, String separator, String keyValueSeparator, String... otherParams) { + return join(map, separator, keyValueSeparator, true, otherParams); + } + + /** + * 将map转成字符串 + * + * @param 键类型 + * @param 值类型 + * @param map Map,为空返回otherParams拼接 + * @param separator entry之间的连接符 + * @param keyValueSeparator kv之间的连接符 + * @param isIgnoreNull 是否忽略null的键和值 + * @param otherParams 其它附加参数字符串(例如密钥) + * @return 连接后的字符串,map和otherParams为空返回"" + * @since 3.1.1 + */ + public static String join(Map map, String separator, String keyValueSeparator, boolean isIgnoreNull, String... otherParams) { + final StringBuilder strBuilder = StrUtil.builder(); + boolean isFirst = true; + if (isNotEmpty(map)) { + for (Entry entry : map.entrySet()) { + if (!isIgnoreNull || entry.getKey() != null && entry.getValue() != null) { + if (isFirst) { + isFirst = false; + } else { + strBuilder.append(separator); + } + strBuilder.append(Convert.toStr(entry.getKey())).append(keyValueSeparator).append(Convert.toStr(entry.getValue())); + } + } + } + // 补充其它字符串到末尾,默认无分隔符 + if (ArrayUtil.isNotEmpty(otherParams)) { + for (String otherParam : otherParams) { + strBuilder.append(otherParam); + } + } + return strBuilder.toString(); + } + + // ----------------------------------------------------------------------------------------------- filter + + /** + * 编辑Map
+ * 编辑过程通过传入的Editor实现来返回需要的元素内容,这个Editor实现可以实现以下功能: + * + *
+	 * 1、过滤出需要的对象,如果返回{@code null}表示这个元素对象抛弃
+	 * 2、修改元素对象,返回集合中为修改后的对象
+	 * 
+ * + * @param Key类型 + * @param Value类型 + * @param map Map + * @param editor 编辑器接口 + * @return 编辑后的Map + */ + @SuppressWarnings("unchecked") + public static Map edit(Map map, Editor> editor) { + if (null == map || null == editor) { + return map; + } + + Map map2 = ReflectUtil.newInstanceIfPossible(map.getClass()); + if (null == map2) { + map2 = new HashMap<>(map.size(), 1f); + } + if (isEmpty(map)) { + return map2; + } + + Entry modified; + for (Entry entry : map.entrySet()) { + modified = editor.edit(entry); + if (null != modified) { + map2.put(modified.getKey(), modified.getValue()); + } + } + return map2; + } + + /** + * 过滤
+ * 过滤过程通过传入的Editor实现来返回需要的元素内容,这个Filter实现可以实现以下功能: + * + *
+	 * 1、过滤出需要的对象,如果返回null表示这个元素对象抛弃
+	 * 
+ * + * @param Key类型 + * @param Value类型 + * @param map Map + * @param filter 过滤器接口,{@code null}返回原Map + * @return 过滤后的Map + * @since 3.1.0 + */ + public static Map filter(Map map, Filter> filter) { + if (null == map || null == filter) { + return map; + } + return edit(map, t -> filter.accept(t) ? t : null); + } + + + /** + * 通过biFunction自定义一个规则,此规则将原Map中的元素转换成新的元素,生成新的Map返回
+ * 变更过程通过传入的 {@link BiFunction} 实现来返回一个值可以为不同类型的 {@link Map} + * + * @param map 原有的map + * @param biFunction {@code lambda},参数包含{@code key},{@code value},返回值会作为新的{@code value} + * @param {@code key}的类型 + * @param {@code value}的类型 + * @param 新的,修改后的{@code value}的类型 + * @return 值可以为不同类型的 {@link Map} + * @since 5.8.0 + */ + public static Map map(Map map, BiFunction biFunction) { + if (null == map || null == biFunction) { + return MapUtil.newHashMap(); + } + return map.entrySet().stream().collect(CollectorUtil.toMap(Entry::getKey, m -> biFunction.apply(m.getKey(), m.getValue()), (l, r) -> l)); + } + + /** + * 过滤Map保留指定键值对,如果键不存在跳过 + * + * @param Key类型 + * @param Value类型 + * @param map 原始Map + * @param keys 键列表,{@code null}返回原Map + * @return Map 结果,结果的Map类型与原Map保持一致 + * @since 4.0.10 + */ + @SuppressWarnings("unchecked") + public static Map filter(Map map, K... keys) { + if (null == map || null == keys) { + return map; + } + + Map map2 = ReflectUtil.newInstanceIfPossible(map.getClass()); + if (null == map2) { + map2 = new HashMap<>(map.size(), 1f); + } + if (isEmpty(map)) { + return map2; + } + + for (K key : keys) { + if (map.containsKey(key)) { + map2.put(key, map.get(key)); + } + } + return map2; + } + + /** + * Map的键和值互换 + * 互换键值对不检查值是否有重复,如果有则后加入的元素替换先加入的元素
+ * 值的顺序在HashMap中不确定,所以谁覆盖谁也不确定,在有序的Map中按照先后顺序覆盖,保留最后的值 + * + * @param 键和值类型 + * @param map Map对象,键值类型必须一致 + * @return 互换后的Map + * @see #inverse(Map) + * @since 3.2.2 + */ + public static Map reverse(Map map) { + return edit(map, t -> new Entry() { + + @Override + public T getKey() { + return t.getValue(); + } + + @Override + public T getValue() { + return t.getKey(); + } + + @Override + public T setValue(T value) { + throw new UnsupportedOperationException("Unsupported setValue method !"); + } + }); + } + + /** + * Map的键和值互换
+ * 互换键值对不检查值是否有重复,如果有则后加入的元素替换先加入的元素
+ * 值的顺序在HashMap中不确定,所以谁覆盖谁也不确定,在有序的Map中按照先后顺序覆盖,保留最后的值 + * + * @param 键和值类型 + * @param 键和值类型 + * @param map Map对象,键值类型必须一致 + * @return 互换后的Map + * @since 5.2.6 + */ + public static Map inverse(Map map) { + final Map result = createMap(map.getClass()); + map.forEach((key, value) -> result.put(value, key)); + return result; + } + + /** + * 排序已有Map,Key有序的Map,使用默认Key排序方式(字母顺序) + * + * @param key的类型 + * @param value的类型 + * @param map Map + * @return TreeMap + * @see #newTreeMap(Map, Comparator) + * @since 4.0.1 + */ + public static TreeMap sort(Map map) { + return sort(map, null); + } + + /** + * 排序已有Map,Key有序的Map + * + * @param key的类型 + * @param value的类型 + * @param map Map,为null返回null + * @param comparator Key比较器 + * @return TreeMap,map为null返回null + * @see #newTreeMap(Map, Comparator) + * @since 4.0.1 + */ + public static TreeMap sort(Map map, Comparator comparator) { + if (null == map) { + return null; + } + + if (map instanceof TreeMap) { + // 已经是可排序Map,此时只有比较器一致才返回原map + TreeMap result = (TreeMap) map; + if (null == comparator || comparator.equals(result.comparator())) { + return result; + } + } + + return newTreeMap(map, comparator); + } + + /** + * 按照值排序,可选是否倒序 + * + * @param map 需要对值排序的map + * @param 键类型 + * @param 值类型 + * @param isDesc 是否倒序 + * @return 排序后新的Map + * @since 5.5.8 + */ + public static > Map sortByValue(Map map, boolean isDesc) { + Map result = new LinkedHashMap<>(); + Comparator> entryComparator = Entry.comparingByValue(); + if (isDesc) { + entryComparator = entryComparator.reversed(); + } + map.entrySet().stream().sorted(entryComparator).forEachOrdered(e -> result.put(e.getKey(), e.getValue())); + return result; + } + + /** + * 创建代理Map
+ * {@link MapProxy}对Map做一次包装,提供各种getXXX方法 + * + * @param map 被代理的Map + * @return {@link MapProxy} + * @since 3.2.0 + */ + public static MapProxy createProxy(Map map) { + return MapProxy.create(map); + } + + /** + * 创建Map包装类MapWrapper
+ * {@link MapWrapper}对Map做一次包装 + * + * @param key的类型 + * @param value的类型 + * @param map 被代理的Map + * @return {@link MapWrapper} + * @since 4.5.4 + */ + public static MapWrapper wrap(Map map) { + return new MapWrapper<>(map); + } + + /** + * 将对应Map转换为不可修改的Map + * + * @param map Map + * @param 键类型 + * @param 值类型 + * @return 不修改Map + * @since 5.2.6 + */ + public static Map unmodifiable(Map map) { + return Collections.unmodifiableMap(map); + } + + // ----------------------------------------------------------------------------------------------- builder + + /** + * 创建链接调用map + * + * @param Key类型 + * @param Value类型 + * @return map创建类 + */ + public static MapBuilder builder() { + return builder(new HashMap<>()); + } + + /** + * 创建链接调用map + * + * @param Key类型 + * @param Value类型 + * @param map 实际使用的map + * @return map创建类 + */ + public static MapBuilder builder(Map map) { + return new MapBuilder<>(map); + } + + /** + * 创建链接调用map + * + * @param Key类型 + * @param Value类型 + * @param k key + * @param v value + * @return map创建类 + */ + public static MapBuilder builder(K k, V v) { + return (builder(new HashMap())).put(k, v); + } + + /** + * 获取Map的部分key生成新的Map + * + * @param Key类型 + * @param Value类型 + * @param map Map + * @param keys 键列表 + * @return 新Map,只包含指定的key + * @since 4.0.6 + */ + @SuppressWarnings("unchecked") + public static Map getAny(Map map, final K... keys) { + return filter(map, entry -> ArrayUtil.contains(keys, entry.getKey())); + } + + /** + * 去掉Map中指定key的键值对,修改原Map + * + * @param Key类型 + * @param Value类型 + * @param map Map + * @param keys 键列表 + * @return 修改后的key + * @since 5.0.5 + */ + @SuppressWarnings("unchecked") + public static Map removeAny(Map map, final K... keys) { + for (K key : keys) { + map.remove(key); + } + return map; + } + + /** + * 获取Map指定key的值,并转换为字符串 + * + * @param map Map + * @param key 键 + * @return 值 + * @since 4.0.6 + */ + public static String getStr(Map map, Object key) { + return get(map, key, String.class); + } + + /** + * 获取Map指定key的值,并转换为字符串 + * + * @param map Map + * @param key 键 + * @param defaultValue 默认值 + * @return 值 + * @since 5.3.11 + */ + public static String getStr(Map map, Object key, String defaultValue) { + return get(map, key, String.class, defaultValue); + } + + /** + * 获取Map指定key的值,并转换为Integer + * + * @param map Map + * @param key 键 + * @return 值 + * @since 4.0.6 + */ + public static Integer getInt(Map map, Object key) { + return get(map, key, Integer.class); + } + + /** + * 获取Map指定key的值,并转换为Integer + * + * @param map Map + * @param key 键 + * @param defaultValue 默认值 + * @return 值 + * @since 5.3.11 + */ + public static Integer getInt(Map map, Object key, Integer defaultValue) { + return get(map, key, Integer.class, defaultValue); + } + + /** + * 获取Map指定key的值,并转换为Double + * + * @param map Map + * @param key 键 + * @return 值 + * @since 4.0.6 + */ + public static Double getDouble(Map map, Object key) { + return get(map, key, Double.class); + } + + /** + * 获取Map指定key的值,并转换为Double + * + * @param map Map + * @param key 键 + * @param defaultValue 默认值 + * @return 值 + * @since 5.3.11 + */ + public static Double getDouble(Map map, Object key, Double defaultValue) { + return get(map, key, Double.class, defaultValue); + } + + /** + * 获取Map指定key的值,并转换为Float + * + * @param map Map + * @param key 键 + * @return 值 + * @since 4.0.6 + */ + public static Float getFloat(Map map, Object key) { + return get(map, key, Float.class); + } + + /** + * 获取Map指定key的值,并转换为Float + * + * @param map Map + * @param key 键 + * @param defaultValue 默认值 + * @return 值 + * @since 5.3.11 + */ + public static Float getFloat(Map map, Object key, Float defaultValue) { + return get(map, key, Float.class, defaultValue); + } + + /** + * 获取Map指定key的值,并转换为Short + * + * @param map Map + * @param key 键 + * @return 值 + * @since 4.0.6 + */ + public static Short getShort(Map map, Object key) { + return get(map, key, Short.class); + } + + /** + * 获取Map指定key的值,并转换为Short + * + * @param map Map + * @param key 键 + * @param defaultValue 默认值 + * @return 值 + * @since 5.3.11 + */ + public static Short getShort(Map map, Object key, Short defaultValue) { + return get(map, key, Short.class, defaultValue); + } + + /** + * 获取Map指定key的值,并转换为Bool + * + * @param map Map + * @param key 键 + * @return 值 + * @since 4.0.6 + */ + public static Boolean getBool(Map map, Object key) { + return get(map, key, Boolean.class); + } + + /** + * 获取Map指定key的值,并转换为Bool + * + * @param map Map + * @param key 键 + * @param defaultValue 默认值 + * @return 值 + * @since 5.3.11 + */ + public static Boolean getBool(Map map, Object key, Boolean defaultValue) { + return get(map, key, Boolean.class, defaultValue); + } + + /** + * 获取Map指定key的值,并转换为Character + * + * @param map Map + * @param key 键 + * @return 值 + * @since 4.0.6 + */ + public static Character getChar(Map map, Object key) { + return get(map, key, Character.class); + } + + /** + * 获取Map指定key的值,并转换为Character + * + * @param map Map + * @param key 键 + * @param defaultValue 默认值 + * @return 值 + * @since 5.3.11 + */ + public static Character getChar(Map map, Object key, Character defaultValue) { + return get(map, key, Character.class, defaultValue); + } + + /** + * 获取Map指定key的值,并转换为Long + * + * @param map Map + * @param key 键 + * @return 值 + * @since 4.0.6 + */ + public static Long getLong(Map map, Object key) { + return get(map, key, Long.class); + } + + /** + * 获取Map指定key的值,并转换为Long + * + * @param map Map + * @param key 键 + * @param defaultValue 默认值 + * @return 值 + * @since 5.3.11 + */ + public static Long getLong(Map map, Object key, Long defaultValue) { + return get(map, key, Long.class, defaultValue); + } + + /** + * 获取Map指定key的值,并转换为{@link Date} + * + * @param map Map + * @param key 键 + * @return 值 + * @since 4.1.2 + */ + public static Date getDate(Map map, Object key) { + return get(map, key, Date.class); + } + + /** + * 获取Map指定key的值,并转换为{@link Date} + * + * @param map Map + * @param key 键 + * @param defaultValue 默认值 + * @return 值 + * @since 4.1.2 + */ + public static Date getDate(Map map, Object key, Date defaultValue) { + return get(map, key, Date.class, defaultValue); + } + + /** + * 获取Map指定key的值,并转换为指定类型 + * + * @param 目标值类型 + * @param map Map + * @param key 键 + * @param type 值类型 + * @return 值 + * @since 4.0.6 + */ + public static T get(Map map, Object key, Class type) { + return get(map, key, type, null); + } + + /** + * 获取Map指定key的值,并转换为指定类型 + * + * @param 目标值类型 + * @param map Map + * @param key 键 + * @param type 值类型 + * @param defaultValue 默认值 + * @return 值 + * @since 5.3.11 + */ + public static T get(Map map, Object key, Class type, T defaultValue) { + return null == map ? defaultValue : Convert.convert(type, map.get(key), defaultValue); + } + + /** + * 获取Map指定key的值,并转换为指定类型,此方法在转换失败后不抛异常,返回null。 + * + * @param 目标值类型 + * @param map Map + * @param key 键 + * @param type 值类型 + * @param defaultValue 默认值 + * @return 值 + * @since 5.5.3 + */ + public static T getQuietly(Map map, Object key, Class type, T defaultValue) { + return null == map ? defaultValue : Convert.convertQuietly(type, map.get(key), defaultValue); + } + + /** + * 获取Map指定key的值,并转换为指定类型 + * + * @param 目标值类型 + * @param map Map + * @param key 键 + * @param type 值类型 + * @return 值 + * @since 4.5.12 + */ + public static T get(Map map, Object key, TypeReference type) { + return get(map, key, type, null); + } + + /** + * 获取Map指定key的值,并转换为指定类型 + * + * @param 目标值类型 + * @param map Map + * @param key 键 + * @param type 值类型 + * @param defaultValue 默认值 + * @return 值 + * @since 5.3.11 + */ + public static T get(Map map, Object key, TypeReference type, T defaultValue) { + return null == map ? defaultValue : Convert.convert(type, map.get(key), defaultValue); + } + + /** + * 获取Map指定key的值,并转换为指定类型,转换失败后返回null,不抛异常 + * + * @param 目标值类型 + * @param map Map + * @param key 键 + * @param type 值类型 + * @param defaultValue 默认值 + * @return 值 + * @since 5.5.3 + */ + public static T getQuietly(Map map, Object key, TypeReference type, T defaultValue) { + return null == map ? defaultValue : Convert.convertQuietly(type, map.get(key), defaultValue); + } + + /** + * 重命名键
+ * 实现方式为一处然后重新put,当旧的key不存在直接返回
+ * 当新的key存在,抛出{@link IllegalArgumentException} 异常 + * + * @param key的类型 + * @param value的类型 + * @param map Map + * @param oldKey 原键 + * @param newKey 新键 + * @return map + * @throws IllegalArgumentException 新key存在抛出此异常 + * @since 4.5.16 + */ + public static Map renameKey(Map map, K oldKey, K newKey) { + if (isNotEmpty(map) && map.containsKey(oldKey)) { + if (map.containsKey(newKey)) { + throw new IllegalArgumentException(StrUtil.format("The key '{}' exist !", newKey)); + } + map.put(newKey, map.remove(oldKey)); + } + return map; + } + + /** + * 去除Map中值为{@code null}的键值对
+ * 注意:此方法在传入的Map上直接修改。 + * + * @param key的类型 + * @param value的类型 + * @param map Map + * @return map + * @since 4.6.5 + */ + public static Map removeNullValue(Map map) { + if (isEmpty(map)) { + return map; + } + + final Iterator> iter = map.entrySet().iterator(); + Entry entry; + while (iter.hasNext()) { + entry = iter.next(); + if (null == entry.getValue()) { + iter.remove(); + } + } + + return map; + } + + /** + * 返回一个空Map + * + * @param 键类型 + * @param 值类型 + * @return 空Map + * @see Collections#emptyMap() + * @since 5.3.1 + */ + public static Map empty() { + return Collections.emptyMap(); + } + + /** + * 根据传入的Map类型不同,返回对应类型的空Map,支持类型包括: + * + *
+	 *     1. NavigableMap
+	 *     2. SortedMap
+	 *     3. Map
+	 * 
+ * + * @param 键类型 + * @param 值类型 + * @param Map类型 + * @param mapClass Map类型,null返回默认的Map + * @return 空Map + * @since 5.3.1 + */ + @SuppressWarnings("unchecked") + public static > T empty(Class mapClass) { + if (null == mapClass) { + return (T) Collections.emptyMap(); + } + if (NavigableMap.class == mapClass) { + return (T) Collections.emptyNavigableMap(); + } else if (SortedMap.class == mapClass) { + return (T) Collections.emptySortedMap(); + } else if (Map.class == mapClass) { + return (T) Collections.emptyMap(); + } + + // 不支持空集合的集合类型 + throw new IllegalArgumentException(StrUtil.format("[{}] is not support to get empty!", mapClass)); + } + + /** + * 清除一个或多个Map集合内的元素,每个Map调用clear()方法 + * + * @param maps 一个或多个Map + */ + public static void clear(Map... maps) { + for (Map map : maps) { + if (isNotEmpty(map)) { + map.clear(); + } + } + } + + /** + * 从Map中获取指定键列表对应的值列表
+ * 如果key在map中不存在或key对应值为null,则返回值列表对应位置的值也为null + * + * @param 键类型 + * @param 值类型 + * @param map {@link Map} + * @param keys 键列表 + * @return 值列表 + * @since 5.7.20 + */ + public static ArrayList valuesOfKeys(Map map, Iterator keys) { + final ArrayList values = new ArrayList<>(); + while (keys.hasNext()) { + values.add(map.get(keys.next())); + } + return values; + } + + /** + * 将键和值转换为{@link AbstractMap.SimpleImmutableEntry}
+ * 返回的Entry不可变 + * + * @param key 键 + * @param value 值 + * @param 键类型 + * @param 值类型 + * @return {@link AbstractMap.SimpleImmutableEntry} + * @since 5.8.0 + */ + public static Entry entry(K key, V value) { + return entry(key, value, true); + } + + /** + * 将键和值转换为{@link AbstractMap.SimpleEntry} 或者 {@link AbstractMap.SimpleImmutableEntry} + * + * @param key 键 + * @param value 值 + * @param 键类型 + * @param 值类型 + * @param isImmutable 是否不可变Entry + * @return {@link AbstractMap.SimpleEntry} 或者 {@link AbstractMap.SimpleImmutableEntry} + * @since 5.8.0 + */ + public static Entry entry(K key, V value, boolean isImmutable) { + return isImmutable ? + new AbstractMap.SimpleImmutableEntry<>(key, value) : + new AbstractMap.SimpleEntry<>(key, value); + } + + /** + * 如果 key 对应的 value 不存在,则使用获取 mappingFunction 重新计算后的值,并保存为该 key 的 value,否则返回 value。
+ * 方法来自Dubbo,解决使用ConcurrentHashMap.computeIfAbsent导致的死循环问题。(issues#2349)
+ * A temporary workaround for Java 8 specific performance issue JDK-8161372 .
+ * This class should be removed once we drop Java 8 support. + * + * @param 键类型 + * @param 值类型 + * @param map Map + * @param key 键 + * @param mappingFunction 值不存在时值的生成函数 + * @see https://bugs.openjdk.java.net/browse/JDK-8161372 + * @return 值 + */ + public static V computeIfAbsent(Map map, K key, Function mappingFunction) { + V value = map.get(key); + if (null == value) { + map.putIfAbsent(key, mappingFunction.apply(key)); + value = map.get(key); + } + return value; + } +} diff --git a/src/main/java/cn/hutool/core/map/MapWrapper.java b/src/main/java/cn/hutool/core/map/MapWrapper.java new file mode 100644 index 0000000..c54a581 --- /dev/null +++ b/src/main/java/cn/hutool/core/map/MapWrapper.java @@ -0,0 +1,236 @@ +package cn.hutool.core.map; + +import cn.hutool.core.util.ObjectUtil; + +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.Serializable; +import java.util.Collection; +import java.util.Iterator; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.function.BiConsumer; +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.function.Supplier; + +/** + * Map包装类,通过包装一个已有Map实现特定功能。例如自定义Key的规则或Value规则 + * + * @param 键类型 + * @param 值类型 + * @author looly + * @since 4.3.3 + */ +public class MapWrapper implements Map, Iterable>, Serializable, Cloneable { + private static final long serialVersionUID = -7524578042008586382L; + + /** + * 默认增长因子 + */ + protected static final float DEFAULT_LOAD_FACTOR = 0.75f; + /** + * 默认初始大小 + */ + protected static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16 + + private Map raw; + + /** + * 构造
+ * 通过传入一个Map从而确定Map的类型,子类需创建一个空的Map,而非传入一个已有Map,否则值可能会被修改 + * + * @param mapFactory 空Map创建工厂 + * @since 5.8.0 + */ + public MapWrapper(Supplier> mapFactory) { + this(mapFactory.get()); + } + + /** + * 构造 + * + * @param raw 被包装的Map + */ + public MapWrapper(Map raw) { + this.raw = raw; + } + + /** + * 获取原始的Map + * + * @return Map + */ + public Map getRaw() { + return this.raw; + } + + @Override + public int size() { + return raw.size(); + } + + @Override + public boolean isEmpty() { + return raw.isEmpty(); + } + + @Override + public boolean containsKey(Object key) { + return raw.containsKey(key); + } + + @Override + public boolean containsValue(Object value) { + return raw.containsValue(value); + } + + @Override + public V get(Object key) { + return raw.get(key); + } + + @Override + public V put(K key, V value) { + return raw.put(key, value); + } + + @Override + public V remove(Object key) { + return raw.remove(key); + } + + @Override + public void putAll(Map m) { + raw.putAll(m); + } + + @Override + public void clear() { + raw.clear(); + } + + @Override + public Collection values() { + return raw.values(); + } + + @Override + public Set keySet() { + return raw.keySet(); + } + + @Override + public Set> entrySet() { + return raw.entrySet(); + } + + @Override + public Iterator> iterator() { + return this.entrySet().iterator(); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + MapWrapper that = (MapWrapper) o; + return Objects.equals(raw, that.raw); + } + + @Override + public int hashCode() { + return Objects.hash(raw); + } + + @Override + public String toString() { + return raw.toString(); + } + + + @Override + public void forEach(BiConsumer action) { + raw.forEach(action); + } + + @Override + public void replaceAll(BiFunction function) { + raw.replaceAll(function); + } + + @Override + public V putIfAbsent(K key, V value) { + return raw.putIfAbsent(key, value); + } + + @Override + public boolean remove(Object key, Object value) { + return raw.remove(key, value); + } + + @Override + public boolean replace(K key, V oldValue, V newValue) { + return raw.replace(key, oldValue, newValue); + } + + @Override + public V replace(K key, V value) { + return raw.replace(key, value); + } + + @Override + public V computeIfAbsent(K key, Function mappingFunction) { + return raw.computeIfAbsent(key, mappingFunction); + } + + // 重写默认方法的意义在于,如果被包装的Map自定义了这些默认方法,包装类就可以保持这些行为的一致性 + //---------------------------------------------------------------------------- Override default methods start + @Override + public V getOrDefault(Object key, V defaultValue) { + return raw.getOrDefault(key, defaultValue); + } + + @Override + public V computeIfPresent(K key, BiFunction remappingFunction) { + return raw.computeIfPresent(key, remappingFunction); + } + + @Override + public V compute(K key, BiFunction remappingFunction) { + return raw.compute(key, remappingFunction); + } + + @Override + public V merge(K key, V value, BiFunction remappingFunction) { + return raw.merge(key, value, remappingFunction); + } + + @Override + public MapWrapper clone() throws CloneNotSupportedException { + @SuppressWarnings("unchecked") final MapWrapper clone = (MapWrapper) super.clone(); + clone.raw = ObjectUtil.clone(raw); + return clone; + } + + //---------------------------------------------------------------------------- Override default methods end + + // region 序列化与反序列化重写 + private void writeObject(final ObjectOutputStream out) throws IOException { + out.defaultWriteObject(); + out.writeObject(this.raw); + } + + @SuppressWarnings("unchecked") + private void readObject(final ObjectInputStream in) throws IOException, ClassNotFoundException { + in.defaultReadObject(); + raw = (Map) in.readObject(); + } + // endregion +} diff --git a/src/main/java/cn/hutool/core/map/ReferenceConcurrentMap.java b/src/main/java/cn/hutool/core/map/ReferenceConcurrentMap.java new file mode 100644 index 0000000..99e80a8 --- /dev/null +++ b/src/main/java/cn/hutool/core/map/ReferenceConcurrentMap.java @@ -0,0 +1,322 @@ +package cn.hutool.core.map; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.lang.func.Func0; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.ReferenceUtil; + +import java.io.Serializable; +import java.lang.ref.Reference; +import java.lang.ref.ReferenceQueue; +import java.lang.ref.SoftReference; +import java.lang.ref.WeakReference; +import java.util.AbstractMap; +import java.util.Collection; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentMap; +import java.util.function.BiConsumer; +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * 线程安全的ReferenceMap实现
+ * 参考:jdk.management.resource.internal.WeakKeyConcurrentHashMap + * + * @param 键类型 + * @param 值类型 + * @author looly + * @since 5.8.0 + */ +public class ReferenceConcurrentMap implements ConcurrentMap, Iterable>, Serializable { + + final ConcurrentMap, V> raw; + private final ReferenceQueue lastQueue; + private final ReferenceUtil.ReferenceType keyType; + /** + * 回收监听 + */ + private BiConsumer, V> purgeListener; + + // region 构造 + + /** + * 构造 + * + * @param raw {@link ConcurrentMap}实现 + * @param referenceType Reference类型 + */ + public ReferenceConcurrentMap(ConcurrentMap, V> raw, ReferenceUtil.ReferenceType referenceType) { + this.raw = raw; + this.keyType = referenceType; + lastQueue = new ReferenceQueue<>(); + } + // endregion + + /** + * 设置对象回收清除监听 + * + * @param purgeListener 监听函数 + */ + public void setPurgeListener(BiConsumer, V> purgeListener) { + this.purgeListener = purgeListener; + } + + @Override + public int size() { + this.purgeStaleKeys(); + return this.raw.size(); + } + + @Override + public boolean isEmpty() { + return 0 == size(); + } + + @Override + public V get(Object key) { + this.purgeStaleKeys(); + //noinspection unchecked + return this.raw.get(ofKey((K) key, null)); + } + + @Override + public boolean containsKey(Object key) { + this.purgeStaleKeys(); + //noinspection unchecked + return this.raw.containsKey(ofKey((K) key, null)); + } + + @Override + public boolean containsValue(Object value) { + this.purgeStaleKeys(); + return this.raw.containsValue(value); + } + + @Override + public V put(K key, V value) { + this.purgeStaleKeys(); + return this.raw.put(ofKey(key, this.lastQueue), value); + } + + @Override + public V putIfAbsent(K key, V value) { + this.purgeStaleKeys(); + return this.raw.putIfAbsent(ofKey(key, this.lastQueue), value); + } + + @Override + public void putAll(Map m) { + m.forEach(this::put); + } + + @Override + public V replace(K key, V value) { + this.purgeStaleKeys(); + return this.raw.replace(ofKey(key, this.lastQueue), value); + } + + @Override + public boolean replace(K key, V oldValue, V newValue) { + this.purgeStaleKeys(); + return this.raw.replace(ofKey(key, this.lastQueue), oldValue, newValue); + } + + @Override + public void replaceAll(BiFunction function) { + this.purgeStaleKeys(); + this.raw.replaceAll((kWeakKey, value) -> function.apply(kWeakKey.get(), value)); + } + + @Override + public V computeIfAbsent(K key, Function mappingFunction) { + this.purgeStaleKeys(); + return this.raw.computeIfAbsent(ofKey(key, this.lastQueue), kWeakKey -> mappingFunction.apply(key)); + } + + @Override + public V computeIfPresent(K key, BiFunction remappingFunction) { + this.purgeStaleKeys(); + return this.raw.computeIfPresent(ofKey(key, this.lastQueue), (kWeakKey, value) -> remappingFunction.apply(key, value)); + } + + /** + * 从缓存中获得对象,当对象不在缓存中或已经过期返回Func0回调产生的对象 + * + * @param key 键 + * @param supplier 如果不存在回调方法,用于生产值对象 + * @return 值对象 + */ + public V computeIfAbsent(K key, Func0 supplier) { + return computeIfAbsent(key, (keyParam) -> supplier.callWithRuntimeException()); + } + + @Override + public V remove(Object key) { + this.purgeStaleKeys(); + //noinspection unchecked + return this.raw.remove(ofKey((K) key, null)); + } + + @Override + public boolean remove(Object key, Object value) { + this.purgeStaleKeys(); + //noinspection unchecked + return this.raw.remove(ofKey((K) key, null), value); + } + + @Override + public void clear() { + this.raw.clear(); + //noinspection StatementWithEmptyBody + while (lastQueue.poll() != null) ; + } + + @Override + public Set keySet() { + // TODO 非高效方式的set转换,应该返回一个view + final Collection trans = CollUtil.trans(this.raw.keySet(), (reference) -> null == reference ? null : reference.get()); + return new HashSet<>(trans); + } + + @Override + public Collection values() { + this.purgeStaleKeys(); + return this.raw.values(); + } + + @Override + public Set> entrySet() { + this.purgeStaleKeys(); + return this.raw.entrySet().stream() + .map(entry -> new AbstractMap.SimpleImmutableEntry<>(entry.getKey().get(), entry.getValue())) + .collect(Collectors.toSet()); + } + + @Override + public void forEach(BiConsumer action) { + this.purgeStaleKeys(); + this.raw.forEach((key, value)-> action.accept(key.get(), value)); + } + + @Override + public Iterator> iterator() { + return entrySet().iterator(); + } + + @Override + public V compute(K key, BiFunction remappingFunction) { + this.purgeStaleKeys(); + return this.raw.compute(ofKey(key, this.lastQueue), (kWeakKey, value) -> remappingFunction.apply(key, value)); + } + + @Override + public V merge(K key, V value, BiFunction remappingFunction) { + this.purgeStaleKeys(); + return this.raw.merge(ofKey(key, this.lastQueue), value, remappingFunction); + } + + /** + * 清除被回收的键 + */ + private void purgeStaleKeys() { + Reference reference; + V value; + while ((reference = this.lastQueue.poll()) != null) { + value = this.raw.remove(reference); + if (null != purgeListener) { + purgeListener.accept(reference, value); + } + } + } + + /** + * 根据Reference类型构建key对应的{@link Reference} + * + * @param key 键 + * @param queue {@link ReferenceQueue} + * @return {@link Reference} + */ + private Reference ofKey(K key, ReferenceQueue queue) { + switch (keyType) { + case WEAK: + return new WeakKey<>(key, queue); + case SOFT: + return new SoftKey<>(key, queue); + } + throw new IllegalArgumentException("Unsupported key type: " + keyType); + } + + /** + * 弱键 + * + * @param 键类型 + */ + private static class WeakKey extends WeakReference { + private final int hashCode; + + /** + * 构造 + * + * @param key 原始Key,不能为{@code null} + * @param queue {@link ReferenceQueue} + */ + WeakKey(K key, ReferenceQueue queue) { + super(key, queue); + hashCode = key.hashCode(); + } + + @Override + public int hashCode() { + return hashCode; + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } else if (other instanceof WeakKey) { + return ObjectUtil.equals(((WeakKey) other).get(), get()); + } + return false; + } + } + + /** + * 弱键 + * + * @param 键类型 + */ + private static class SoftKey extends SoftReference { + private final int hashCode; + + /** + * 构造 + * + * @param key 原始Key,不能为{@code null} + * @param queue {@link ReferenceQueue} + */ + SoftKey(K key, ReferenceQueue queue) { + super(key, queue); + hashCode = key.hashCode(); + } + + @Override + public int hashCode() { + return hashCode; + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } else if (other instanceof SoftKey) { + return ObjectUtil.equals(((SoftKey) other).get(), get()); + } + return false; + } + } +} diff --git a/src/main/java/cn/hutool/core/map/SafeConcurrentHashMap.java b/src/main/java/cn/hutool/core/map/SafeConcurrentHashMap.java new file mode 100644 index 0000000..4eccc90 --- /dev/null +++ b/src/main/java/cn/hutool/core/map/SafeConcurrentHashMap.java @@ -0,0 +1,74 @@ +package cn.hutool.core.map; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Function; + +/** + * 安全的ConcurrentHashMap实现
+ * 此类用于解决在JDK8中调用{@link ConcurrentHashMap#computeIfAbsent(Object, Function)}可能造成的死循环问题。
+ * 方法来自Dubbo,见:issues#2349
+ *

+ * 相关bug见:@see https://bugs.openjdk.java.net/browse/JDK-8161372 + * + * @param 键类型 + * @param 值类型 + */ +public class SafeConcurrentHashMap extends ConcurrentHashMap { + private static final long serialVersionUID = 1L; + + // region == 构造 == + + /** + * 构造,默认初始大小(16) + */ + public SafeConcurrentHashMap() { + super(); + } + + /** + * 构造 + * + * @param initialCapacity 预估初始大小 + */ + public SafeConcurrentHashMap(int initialCapacity) { + super(initialCapacity); + } + + /** + * 构造 + * + * @param m 初始键值对 + */ + public SafeConcurrentHashMap(Map m) { + super(m); + } + + /** + * 构造 + * + * @param initialCapacity 初始容量 + * @param loadFactor 增长系数 + */ + public SafeConcurrentHashMap(int initialCapacity, float loadFactor) { + super(initialCapacity, loadFactor); + } + + /** + * 构造 + * + * @param initialCapacity 初始容量 + * @param loadFactor 增长系数 + * @param concurrencyLevel 并发级别,即Segment的个数 + */ + public SafeConcurrentHashMap(int initialCapacity, + float loadFactor, int concurrencyLevel) { + super(initialCapacity, loadFactor, concurrencyLevel); + } + // endregion == 构造 == + + @Override + public V computeIfAbsent(K key, Function mappingFunction) { + return MapUtil.computeIfAbsent(this, key, mappingFunction); + } +} diff --git a/src/main/java/cn/hutool/core/map/TableMap.java b/src/main/java/cn/hutool/core/map/TableMap.java new file mode 100644 index 0000000..14eff1c --- /dev/null +++ b/src/main/java/cn/hutool/core/map/TableMap.java @@ -0,0 +1,330 @@ +package cn.hutool.core.map; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.collection.ListUtil; +import cn.hutool.core.util.ObjUtil; +import cn.hutool.core.util.ObjectUtil; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.BiConsumer; +import java.util.function.BiFunction; + +/** + * 可重复键和值的Map
+ * 通过键值单独建立List方式,使键值对一一对应,实现正向和反向两种查找
+ * 无论是正向还是反向,都是遍历列表查找过程,相比标准的HashMap要慢,数据越多越慢 + * + * @param 键类型 + * @param 值类型 + * @author looly + */ +public class TableMap implements Map, Iterable>, Serializable { + private static final long serialVersionUID = 1L; + + private static final int DEFAULT_CAPACITY = 10; + + private final List keys; + private final List values; + + /** + * 构造 + */ + public TableMap() { + this(DEFAULT_CAPACITY); + } + + /** + * 构造 + * + * @param size 初始容量 + */ + public TableMap(int size) { + this.keys = new ArrayList<>(size); + this.values = new ArrayList<>(size); + } + + /** + * 构造 + * + * @param keys 键列表 + * @param values 值列表 + */ + public TableMap(K[] keys, V[] values) { + this.keys = CollUtil.toList(keys); + this.values = CollUtil.toList(values); + } + + @Override + public int size() { + return keys.size(); + } + + @Override + public boolean isEmpty() { + return CollUtil.isEmpty(keys); + } + + @Override + public boolean containsKey(Object key) { + //noinspection SuspiciousMethodCalls + return keys.contains(key); + } + + @Override + public boolean containsValue(Object value) { + //noinspection SuspiciousMethodCalls + return values.contains(value); + } + + @Override + public V get(Object key) { + //noinspection SuspiciousMethodCalls + final int index = keys.indexOf(key); + if (index > -1) { + return values.get(index); + } + return null; + } + + /** + * 根据value获得对应的key,只返回找到的第一个value对应的key值 + * + * @param value 值 + * @return 键 + * @since 5.3.3 + */ + public K getKey(V value) { + final int index = values.indexOf(value); + if (index > -1) { + return keys.get(index); + } + return null; + } + + /** + * 获取指定key对应的所有值 + * + * @param key 键 + * @return 值列表 + * @since 5.2.5 + */ + public List getValues(K key) { + return CollUtil.getAny( + this.values, + ListUtil.indexOfAll(this.keys, (ele) -> ObjectUtil.equal(ele, key)) + ); + } + + /** + * 获取指定value对应的所有key + * + * @param value 值 + * @return 值列表 + * @since 5.2.5 + */ + public List getKeys(V value) { + return CollUtil.getAny( + this.keys, + ListUtil.indexOfAll(this.values, (ele) -> ObjectUtil.equal(ele, value)) + ); + } + + @Override + public V put(K key, V value) { + keys.add(key); + values.add(value); + return null; + } + + /** + * 移除指定的所有键和对应的所有值 + * + * @param key 键 + * @return 最后一个移除的值 + */ + @Override + public V remove(Object key) { + V lastValue = null; + int index; + //noinspection SuspiciousMethodCalls + while ((index = keys.indexOf(key)) > -1) { + lastValue = removeByIndex(index); + } + return lastValue; + } + + /** + * 移除指定位置的键值对 + * + * @param index 位置,不能越界 + * @return 移除的值 + */ + public V removeByIndex(final int index) { + keys.remove(index); + return values.remove(index); + } + + @Override + public void putAll(Map m) { + for (Entry entry : m.entrySet()) { + this.put(entry.getKey(), entry.getValue()); + } + } + + @Override + public void clear() { + keys.clear(); + values.clear(); + } + + @Override + public Set keySet() { + return new HashSet<>(this.keys); + } + + /** + * 获取所有键,可重复,不可修改 + * + * @return 键列表 + * @since 5.8.0 + */ + public List keys() { + return Collections.unmodifiableList(this.keys); + } + + @Override + public Collection values() { + return Collections.unmodifiableList(this.values); + } + + @Override + public Set> entrySet() { + final Set> hashSet = new LinkedHashSet<>(); + for (int i = 0; i < size(); i++) { + hashSet.add(MapUtil.entry(keys.get(i), values.get(i))); + } + return hashSet; + } + + @Override + public Iterator> iterator() { + return new Iterator>() { + private final Iterator keysIter = keys.iterator(); + private final Iterator valuesIter = values.iterator(); + + @Override + public boolean hasNext() { + return keysIter.hasNext() && valuesIter.hasNext(); + } + + @Override + public Entry next() { + return MapUtil.entry(keysIter.next(), valuesIter.next()); + } + + @Override + public void remove() { + keysIter.remove(); + valuesIter.remove(); + } + }; + } + + @Override + public String toString() { + return "TableMap{" + + "keys=" + keys + + ", values=" + values + + '}'; + } + + @Override + public void forEach(final BiConsumer action) { + for (int i = 0; i < size(); i++) { + action.accept(keys.get(i), values.get(i)); + } + } + + @Override + public boolean remove(final Object key, final Object value) { + boolean removed = false; + for (int i = 0; i < size(); i++) { + if (ObjUtil.equals(key, keys.get(i)) && ObjUtil.equals(value, values.get(i))) { + removeByIndex(i); + removed = true; + // 移除当前元素,下个元素前移 + i--; + } + } + return removed; + } + + @Override + public void replaceAll(final BiFunction function) { + for (int i = 0; i < size(); i++) { + final V newValue = function.apply(keys.get(i), values.get(i)); + values.set(i, newValue); + } + } + + @Override + public boolean replace(final K key, final V oldValue, final V newValue) { + for (int i = 0; i < size(); i++) { + if (ObjUtil.equals(key, keys.get(i)) && ObjUtil.equals(oldValue, values.get(i))) { + values.set(i, newValue); + return true; + } + } + return false; + } + + /** + * 替换指定key的所有值为指定值 + * + * @param key 指定的key + * @param value 替换的值 + * @return 最后替换的值 + */ + @Override + public V replace(final K key, final V value) { + V lastValue = null; + for (int i = 0; i < size(); i++) { + if (ObjUtil.equals(key, keys.get(i))) { + lastValue = values.set(i, value); + } + } + return lastValue; + } + + @SuppressWarnings("NullableProblems") + @Override + public V computeIfPresent(final K key, final BiFunction remappingFunction) { + if(null == remappingFunction){ + return null; + } + + V lastValue = null; + for (int i = 0; i < size(); i++) { + if (ObjUtil.equals(key, keys.get(i))) { + final V newValue = remappingFunction.apply(key, values.get(i)); + if(null != newValue){ + lastValue = values.set(i, newValue); + } else{ + removeByIndex(i); + // 移除当前元素,下个元素前移 + i--; + } + } + } + return lastValue; + } +} diff --git a/src/main/java/cn/hutool/core/map/TolerantMap.java b/src/main/java/cn/hutool/core/map/TolerantMap.java new file mode 100644 index 0000000..80cde27 --- /dev/null +++ b/src/main/java/cn/hutool/core/map/TolerantMap.java @@ -0,0 +1,103 @@ +package cn.hutool.core.map; + +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +/** + * 一个可以提供默认值的Map + * + * @param 键类型 + * @param 值类型 + * @author pantao, looly + */ +public class TolerantMap extends MapWrapper { + private static final long serialVersionUID = -4158133823263496197L; + + private final V defaultValue; + + /** + * 构造 + * + * @param defaultValue 默认值 + */ + public TolerantMap(V defaultValue) { + this(new HashMap<>(), defaultValue); + } + + /** + * 构造 + * + * @param initialCapacity 初始容量 + * @param loadFactor 增长因子 + * @param defaultValue 默认值 + */ + public TolerantMap(int initialCapacity, float loadFactor, V defaultValue) { + this(new HashMap<>(initialCapacity, loadFactor), defaultValue); + } + + /** + * 构造 + * + * @param initialCapacity 初始容量 + * @param defaultValue 默认值 + */ + public TolerantMap(int initialCapacity, V defaultValue) { + this(new HashMap<>(initialCapacity), defaultValue); + } + + /** + * 构造 + * + * @param map Map实现 + * @param defaultValue 默认值 + */ + public TolerantMap(Map map, V defaultValue) { + super(map); + this.defaultValue = defaultValue; + } + + /** + * 构建TolerantMap + * + * @param map map实现 + * @param defaultValue 默认值 + * @param 键类型 + * @param 值类型 + * @return TolerantMap + */ + public static TolerantMap of(Map map, V defaultValue) { + return new TolerantMap<>(map, defaultValue); + } + + @Override + public V get(Object key) { + return getOrDefault(key, defaultValue); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + if (!super.equals(o)) { + return false; + } + final TolerantMap that = (TolerantMap) o; + return getRaw().equals(that.getRaw()) + && Objects.equals(defaultValue, that.defaultValue); + } + + @Override + public int hashCode() { + return Objects.hash(getRaw(), defaultValue); + } + + @Override + public String toString() { + return "TolerantMap{" + "map=" + getRaw() + ", defaultValue=" + defaultValue + '}'; + } +} diff --git a/src/main/java/cn/hutool/core/map/TransMap.java b/src/main/java/cn/hutool/core/map/TransMap.java new file mode 100644 index 0000000..08ea433 --- /dev/null +++ b/src/main/java/cn/hutool/core/map/TransMap.java @@ -0,0 +1,129 @@ +package cn.hutool.core.map; + +import java.util.Map; +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.function.Supplier; + +/** + * 自定义键和值转换的的Map
+ * 继承此类后,通过实现{@link #customKey(Object)}和{@link #customValue(Object)},按照给定规则加入到map或获取值。 + * + * @param 键类型 + * @param 值类型 + * @author Looly + * @since 5.8.0 + */ +public abstract class TransMap extends MapWrapper { + private static final long serialVersionUID = 1L; + + /** + * 构造
+ * 通过传入一个Map从而确定Map的类型,子类需创建一个空的Map,而非传入一个已有Map,否则值可能会被修改 + * + * @param mapFactory 空Map创建工厂 + * @since 5.8.0 + */ + public TransMap(Supplier> mapFactory) { + super(mapFactory); + } + + /** + * 构造
+ * 通过传入一个Map从而确定Map的类型,子类需创建一个空的Map,而非传入一个已有Map,否则值可能会被修改 + * + * @param emptyMap Map 被包装的Map,必须为空Map,否则自定义key会无效 + * @since 3.1.2 + */ + public TransMap(Map emptyMap) { + super(emptyMap); + } + + @Override + public V get(Object key) { + return super.get(customKey(key)); + } + + @Override + public V put(K key, V value) { + return super.put(customKey(key), customValue(value)); + } + + @Override + public void putAll(Map m) { + m.forEach(this::put); + } + + @Override + public boolean containsKey(Object key) { + return super.containsKey(customKey(key)); + } + + @Override + public V remove(Object key) { + return super.remove(customKey(key)); + } + + @Override + public boolean remove(Object key, Object value) { + return super.remove(customKey(key), customValue(value)); + } + + @Override + public boolean replace(K key, V oldValue, V newValue) { + return super.replace(customKey(key), customValue(oldValue), customValue(newValue)); + } + + @Override + public V replace(K key, V value) { + return super.replace(customKey(key), customValue(value)); + } + + //---------------------------------------------------------------------------- Override default methods start + @Override + public V getOrDefault(Object key, V defaultValue) { + return super.getOrDefault(customKey(key), customValue(defaultValue)); + } + + @Override + public V computeIfPresent(K key, BiFunction remappingFunction) { + return super.computeIfPresent(customKey(key), (k, v) -> remappingFunction.apply(customKey(k), customValue(v))); + } + + @Override + public V compute(K key, BiFunction remappingFunction) { + return super.compute(customKey(key), (k, v) -> remappingFunction.apply(customKey(k), customValue(v))); + } + + @Override + public V merge(K key, V value, BiFunction remappingFunction) { + return super.merge(customKey(key), customValue(value), (v1, v2) -> remappingFunction.apply(customValue(v1), customValue(v2))); + } + + @Override + public V putIfAbsent(K key, V value) { + return super.putIfAbsent(customKey(key), customValue(value)); + } + + @Override + public V computeIfAbsent(final K key, final Function mappingFunction) { + return super.computeIfAbsent(customKey(key), mappingFunction); + } + //---------------------------------------------------------------------------- Override default methods end + + /** + * 自定义键 + * + * @param key KEY + * @return 自定义KEY + */ + protected abstract K customKey(Object key); + + /** + * 自定义值 + * + * @param value 值 + * @return 自定义值 + */ + protected abstract V customValue(Object value); +} diff --git a/src/main/java/cn/hutool/core/map/TreeEntry.java b/src/main/java/cn/hutool/core/map/TreeEntry.java new file mode 100644 index 0000000..a858568 --- /dev/null +++ b/src/main/java/cn/hutool/core/map/TreeEntry.java @@ -0,0 +1,143 @@ +package cn.hutool.core.map; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ObjectUtil; + +import java.util.Map; +import java.util.function.Consumer; + +/** + * 允许拥有一个父节点与多个子节点的{@link Map.Entry}实现, + * 表示一个以key作为唯一标识,并且可以挂载一个对应值的树节点, + * 提供一些基于该节点对其所在树结构进行访问的方法 + * + * @param 节点的key类型 + * @param 节点的value类型 + * @author huangchengxing + * @see ForestMap + */ +public interface TreeEntry extends Map.Entry { + + // ===================== Entry方法的重定义 ===================== + + /** + * 比较目标对象与当前{@link TreeEntry}是否相等。
+ * 默认只要{@link TreeEntry#getKey()}的返回值相同,即认为两者相等 + * + * @param o 目标对象 + * @return 是否 + */ + @Override + boolean equals(Object o); + + /** + * 返回当前{@link TreeEntry}的哈希值。
+ * 默认总是返回{@link TreeEntry#getKey()}的哈希值 + * + * @return 哈希值 + */ + @Override + int hashCode(); + + // ===================== 父节点相关方法 ===================== + + /** + * 获取以当前节点作为叶子节点的树结构,然后获取当前节点与根节点的距离 + * + * @return 当前节点与根节点的距离 + */ + int getWeight(); + + /** + * 获取以当前节点作为叶子节点的树结构,然后获取该树结构的根节点 + * + * @return 根节点 + */ + TreeEntry getRoot(); + + /** + * 当前节点是否存在直接关联的父节点 + * + * @return 是否 + */ + default boolean hasParent() { + return ObjectUtil.isNotNull(getDeclaredParent()); + } + + /** + * 获取当前节点直接关联的父节点 + * + * @return 父节点,当节点不存在对应父节点时返回null + */ + TreeEntry getDeclaredParent(); + + /** + * 获取以当前节点作为叶子节点的树结构,然后获取该树结构中当前节点的指定父节点 + * + * @param key 指定父节点的key + * @return 指定父节点,当不存在时返回null + */ + TreeEntry getParent(K key); + + /** + * 获取以当前节点作为叶子节点的树结构,然后确认该树结构中当前节点是否存在指定父节点 + * + * @param key 指定父节点的key + * @return 是否 + */ + default boolean containsParent(K key) { + return ObjectUtil.isNotNull(getParent(key)); + } + + // ===================== 子节点相关方法 ===================== + + /** + * 获取以当前节点作为根节点的树结构,然后遍历所有节点 + * + * @param includeSelf 是否处理当前节点 + * @param nodeConsumer 对节点的处理 + */ + void forEachChild(boolean includeSelf, Consumer> nodeConsumer); + + /** + * 获取当前节点直接关联的子节点 + * + * @return 节点 + */ + Map> getDeclaredChildren(); + + /** + * 获取以当前节点作为根节点的树结构,然后获取该树结构中的当前节点的全部子节点 + * + * @return 节点 + */ + Map> getChildren(); + + /** + * 当前节点是否有子节点 + * + * @return 是否 + */ + default boolean hasChildren() { + return CollUtil.isNotEmpty(getDeclaredChildren()); + } + + /** + * 获取以当前节点作为根节点的树结构,然后获取该树结构中的当前节点的指定子节点 + * + * @param key 指定子节点的key + * @return 节点 + */ + TreeEntry getChild(K key); + + /** + * 获取以当前节点作为根节点的树结构,然后确认该树结构中当前节点是否存在指定子节点 + * + * @param key 指定子节点的key + * @return 是否 + */ + default boolean containsChild(K key) { + return ObjectUtil.isNotNull(getChild(key)); + } + +} diff --git a/src/main/java/cn/hutool/core/map/WeakConcurrentMap.java b/src/main/java/cn/hutool/core/map/WeakConcurrentMap.java new file mode 100644 index 0000000..8936960 --- /dev/null +++ b/src/main/java/cn/hutool/core/map/WeakConcurrentMap.java @@ -0,0 +1,34 @@ +package cn.hutool.core.map; + +import cn.hutool.core.util.ReferenceUtil; + +import java.lang.ref.Reference; +import java.util.concurrent.ConcurrentMap; + +/** + * 线程安全的WeakMap实现
+ * 参考:jdk.management.resource.internal.WeakKeyConcurrentHashMap + * + * @param 键类型 + * @param 值类型 + * @author looly + * @since 5.8.0 + */ +public class WeakConcurrentMap extends ReferenceConcurrentMap { + + /** + * 构造 + */ + public WeakConcurrentMap() { + this(new SafeConcurrentHashMap<>()); + } + + /** + * 构造 + * + * @param raw {@link ConcurrentMap}实现 + */ + public WeakConcurrentMap(ConcurrentMap, V> raw) { + super(raw, ReferenceUtil.ReferenceType.WEAK); + } +} diff --git a/src/main/java/cn/hutool/core/map/multi/AbsCollValueMap.java b/src/main/java/cn/hutool/core/map/multi/AbsCollValueMap.java new file mode 100644 index 0000000..3ab7e27 --- /dev/null +++ b/src/main/java/cn/hutool/core/map/multi/AbsCollValueMap.java @@ -0,0 +1,151 @@ +package cn.hutool.core.map.multi; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.map.MapWrapper; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; + +/** + * 值作为集合的Map实现,通过调用putValue可以在相同key时加入多个值,多个值用集合表示 + * + * @param 键类型 + * @param 值类型 + * @param 集合类型 + * @author looly + * @since 5.7.4 + */ +public abstract class AbsCollValueMap> extends MapWrapper { + private static final long serialVersionUID = 1L; + + /** + * 默认集合初始大小 + */ + protected static final int DEFAULT_COLLECTION_INITIAL_CAPACITY = 3; + + // ------------------------------------------------------------------------- Constructor start + + /** + * 构造 + */ + public AbsCollValueMap() { + this(DEFAULT_INITIAL_CAPACITY); + } + + /** + * 构造 + * + * @param initialCapacity 初始大小 + */ + public AbsCollValueMap(int initialCapacity) { + this(initialCapacity, DEFAULT_LOAD_FACTOR); + } + + /** + * 构造 + * + * @param m Map + */ + public AbsCollValueMap(Map m) { + this(DEFAULT_LOAD_FACTOR, m); + } + + /** + * 构造 + * + * @param loadFactor 加载因子 + * @param m Map + */ + public AbsCollValueMap(float loadFactor, Map m) { + this(m.size(), loadFactor); + this.putAll(m); + } + + /** + * 构造 + * + * @param initialCapacity 初始大小 + * @param loadFactor 加载因子 + */ + public AbsCollValueMap(int initialCapacity, float loadFactor) { + super(new HashMap<>(initialCapacity, loadFactor)); + } + // ------------------------------------------------------------------------- Constructor end + + /** + * 放入所有value + * + * @param m valueMap + * @since 5.7.4 + */ + public void putAllValues(Map> m) { + if (null != m) { + m.forEach((key, valueColl) -> { + if (null != valueColl) { + valueColl.forEach((value) -> putValue(key, value)); + } + }); + } + } + + /** + * 放入Value
+ * 如果键对应值列表有值,加入,否则创建一个新列表后加入 + * + * @param key 键 + * @param value 值 + */ + public void putValue(K key, V value) { + C collection = this.get(key); + if (null == collection) { + collection = createCollection(); + this.put(key, collection); + } + collection.add(value); + } + + /** + * 获取值 + * + * @param key 键 + * @param index 第几个值的索引,越界返回null + * @return 值或null + */ + public V get(K key, int index) { + final Collection collection = get(key); + return CollUtil.get(collection, index); + } + + /** + * 移除value集合中的某个值 + * + * @param key 键 + * @param value 集合中的某个值 + * @return 是否删除成功 + */ + public boolean removeValue(K key, V value) { + C collection = this.get(key); + return null != collection && collection.remove(value); + } + + /** + * 移除value集合中的某些值 + * + * @param key 键 + * @param values 集合中的某些值 + * @return 是否删除成功 + */ + public boolean removeValues(K key, Collection values) { + C collection = this.get(key); + return null != collection && collection.removeAll(values); + } + + /** + * 创建集合
+ * 此方法用于创建在putValue后追加值所在的集合,子类实现此方法创建不同类型的集合 + * + * @return {@link Collection} + */ + protected abstract C createCollection(); +} diff --git a/src/main/java/cn/hutool/core/map/multi/AbsTable.java b/src/main/java/cn/hutool/core/map/multi/AbsTable.java new file mode 100644 index 0000000..0b691f1 --- /dev/null +++ b/src/main/java/cn/hutool/core/map/multi/AbsTable.java @@ -0,0 +1,236 @@ +package cn.hutool.core.map.multi; + +import cn.hutool.core.collection.IterUtil; +import cn.hutool.core.collection.TransIter; +import cn.hutool.core.util.ObjectUtil; + +import java.io.Serializable; +import java.util.AbstractCollection; +import java.util.AbstractSet; +import java.util.Collection; +import java.util.Iterator; +import java.util.Map; +import java.util.Objects; +import java.util.Set; + +/** + * 抽象{@link Table}接口实现
+ * 默认实现了: + *

    + *
  • {@link #equals(Object)}
  • + *
  • {@link #hashCode()}
  • + *
  • {@link #toString()}
  • + *
  • {@link #values()}
  • + *
  • {@link #cellSet()}
  • + *
  • {@link #iterator()}
  • + *
+ * + * @param 行类型 + * @param 列类型 + * @param 值类型 + * @author Guava, Looly + * @since 5.7.23 + */ +public abstract class AbsTable implements Table { + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } else if (obj instanceof Table) { + final Table that = (Table) obj; + return this.cellSet().equals(that.cellSet()); + } else { + return false; + } + } + + @Override + public int hashCode() { + return cellSet().hashCode(); + } + + @Override + public String toString() { + return rowMap().toString(); + } + + //region values + @Override + public Collection values() { + Collection result = values; + return (result == null) ? values = new Values() : result; + } + + private Collection values; + private class Values extends AbstractCollection { + @Override + public Iterator iterator() { + return new TransIter<>(cellSet().iterator(), Cell::getValue); + } + + @Override + public boolean contains(Object o) { + //noinspection unchecked + return containsValue((V) o); + } + + @Override + public void clear() { + AbsTable.this.clear(); + } + + @Override + public int size() { + return AbsTable.this.size(); + } + } + //endregion + + //region cellSet + @Override + public Set> cellSet() { + Set> result = cellSet; + return (result == null) ? cellSet = new CellSet() : result; + } + + private Set> cellSet; + + private class CellSet extends AbstractSet> { + @Override + public boolean contains(Object o) { + if (o instanceof Cell) { + @SuppressWarnings("unchecked") final Cell cell = (Cell) o; + Map row = getRow(cell.getRowKey()); + if (null != row) { + return ObjectUtil.equals(row.get(cell.getColumnKey()), cell.getValue()); + } + } + return false; + } + + @Override + public boolean remove(Object o) { + if (contains(o)) { + @SuppressWarnings("unchecked") final Cell cell = (Cell) o; + AbsTable.this.remove(cell.getRowKey(), cell.getColumnKey()); + } + return false; + } + + @Override + public void clear() { + AbsTable.this.clear(); + } + + @Override + public Iterator> iterator() { + return new CellIterator(); + } + + @Override + public int size() { + return AbsTable.this.size(); + } + } + //endregion + + //region iterator + @Override + public Iterator> iterator() { + return new CellIterator(); + } + + /** + * 基于{@link Cell}的{@link Iterator}实现 + */ + private class CellIterator implements Iterator> { + final Iterator>> rowIterator = rowMap().entrySet().iterator(); + Map.Entry> rowEntry; + Iterator> columnIterator = IterUtil.empty(); + + @Override + public boolean hasNext() { + return rowIterator.hasNext() || columnIterator.hasNext(); + } + + @Override + public Cell next() { + if (!columnIterator.hasNext()) { + rowEntry = rowIterator.next(); + columnIterator = rowEntry.getValue().entrySet().iterator(); + } + final Map.Entry columnEntry = columnIterator.next(); + return new SimpleCell<>(rowEntry.getKey(), columnEntry.getKey(), columnEntry.getValue()); + } + + @Override + public void remove() { + columnIterator.remove(); + if (rowEntry.getValue().isEmpty()) { + rowIterator.remove(); + } + } + } + //endregion + + /** + * 简单{@link Cell} 实现 + * + * @param 行类型 + * @param 列类型 + * @param 值类型 + */ + private static class SimpleCell implements Cell, Serializable { + private static final long serialVersionUID = 1L; + + private final R rowKey; + private final C columnKey; + private final V value; + + SimpleCell(R rowKey, C columnKey, V value) { + this.rowKey = rowKey; + this.columnKey = columnKey; + this.value = value; + } + + @Override + public R getRowKey() { + return rowKey; + } + + @Override + public C getColumnKey() { + return columnKey; + } + + @Override + public V getValue() { + return value; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (obj instanceof Cell) { + Cell other = (Cell) obj; + return ObjectUtil.equal(rowKey, other.getRowKey()) + && ObjectUtil.equal(columnKey, other.getColumnKey()) + && ObjectUtil.equal(value, other.getValue()); + } + return false; + } + + @Override + public int hashCode() { + return Objects.hash(rowKey, columnKey, value); + } + + @Override + public String toString() { + return "(" + rowKey + "," + columnKey + ")=" + value; + } + } +} diff --git a/src/main/java/cn/hutool/core/map/multi/CollectionValueMap.java b/src/main/java/cn/hutool/core/map/multi/CollectionValueMap.java new file mode 100644 index 0000000..f16ed22 --- /dev/null +++ b/src/main/java/cn/hutool/core/map/multi/CollectionValueMap.java @@ -0,0 +1,102 @@ +package cn.hutool.core.map.multi; + +import cn.hutool.core.lang.func.Func0; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; + +/** + * 值作为集合的Map实现,通过调用putValue可以在相同key时加入多个值,多个值用集合表示
+ * 此类可以通过传入函数自定义集合类型的创建规则 + * + * @param 键类型 + * @param 值类型 + * @author looly + * @since 4.3.3 + */ +public class CollectionValueMap extends AbsCollValueMap> { + private static final long serialVersionUID = 9012989578038102983L; + + private final Func0> collectionCreateFunc; + + // ------------------------------------------------------------------------- Constructor start + + /** + * 构造 + */ + public CollectionValueMap() { + this(DEFAULT_INITIAL_CAPACITY); + } + + /** + * 构造 + * + * @param initialCapacity 初始大小 + */ + public CollectionValueMap(int initialCapacity) { + this(initialCapacity, DEFAULT_LOAD_FACTOR); + } + + /** + * 构造 + * + * @param m Map + */ + public CollectionValueMap(Map> m) { + this(DEFAULT_LOAD_FACTOR, m); + } + + /** + * 构造 + * + * @param loadFactor 加载因子 + * @param m Map + */ + public CollectionValueMap(float loadFactor, Map> m) { + this(loadFactor, m, ArrayList::new); + } + + /** + * 构造 + * + * @param initialCapacity 初始大小 + * @param loadFactor 加载因子 + */ + public CollectionValueMap(int initialCapacity, float loadFactor) { + this(initialCapacity, loadFactor, ArrayList::new); + } + + /** + * 构造 + * + * @param loadFactor 加载因子 + * @param m Map + * @param collectionCreateFunc Map中值的集合创建函数 + * @since 5.7.4 + */ + public CollectionValueMap(float loadFactor, Map> m, Func0> collectionCreateFunc) { + this(m.size(), loadFactor, collectionCreateFunc); + this.putAll(m); + } + + /** + * 构造 + * + * @param initialCapacity 初始大小 + * @param loadFactor 加载因子 + * @param collectionCreateFunc Map中值的集合创建函数 + * @since 5.7.4 + */ + public CollectionValueMap(int initialCapacity, float loadFactor, Func0> collectionCreateFunc) { + super(new HashMap<>(initialCapacity, loadFactor)); + this.collectionCreateFunc = collectionCreateFunc; + } + // ------------------------------------------------------------------------- Constructor end + + @Override + protected Collection createCollection() { + return collectionCreateFunc.callWithRuntimeException(); + } +} diff --git a/src/main/java/cn/hutool/core/map/multi/ListValueMap.java b/src/main/java/cn/hutool/core/map/multi/ListValueMap.java new file mode 100644 index 0000000..d931c94 --- /dev/null +++ b/src/main/java/cn/hutool/core/map/multi/ListValueMap.java @@ -0,0 +1,73 @@ +package cn.hutool.core.map.multi; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * 值作为集合List的Map实现,通过调用putValue可以在相同key时加入多个值,多个值用集合表示 + * + * @author looly + * + * @param 键类型 + * @param 值类型 + * @since 4.3.3 + */ +public class ListValueMap extends AbsCollValueMap> { + private static final long serialVersionUID = 6044017508487827899L; + + // ------------------------------------------------------------------------- Constructor start + /** + * 构造 + */ + public ListValueMap() { + this(DEFAULT_INITIAL_CAPACITY); + } + + /** + * 构造 + * + * @param initialCapacity 初始大小 + */ + public ListValueMap(int initialCapacity) { + this(initialCapacity, DEFAULT_LOAD_FACTOR); + } + + /** + * 构造 + * + * @param m Map + */ + public ListValueMap(Map> m) { + this(DEFAULT_LOAD_FACTOR, m); + } + + /** + * 构造 + * + * @param loadFactor 加载因子 + * @param m Map + */ + public ListValueMap(float loadFactor, Map> m) { + this(m.size(), loadFactor); + this.putAllValues(m); + } + + /** + * 构造 + * + * @param initialCapacity 初始大小 + * @param loadFactor 加载因子 + */ + public ListValueMap(int initialCapacity, float loadFactor) { + super(new HashMap<>(initialCapacity, loadFactor)); + } + // ------------------------------------------------------------------------- Constructor end + + @Override + protected List createCollection() { + return new ArrayList<>(DEFAULT_COLLECTION_INITIAL_CAPACITY); + } +} diff --git a/src/main/java/cn/hutool/core/map/multi/RowKeyTable.java b/src/main/java/cn/hutool/core/map/multi/RowKeyTable.java new file mode 100644 index 0000000..aa24547 --- /dev/null +++ b/src/main/java/cn/hutool/core/map/multi/RowKeyTable.java @@ -0,0 +1,281 @@ +package cn.hutool.core.map.multi; + +import cn.hutool.core.builder.Builder; +import cn.hutool.core.collection.ComputeIter; +import cn.hutool.core.collection.IterUtil; +import cn.hutool.core.collection.TransIter; +import cn.hutool.core.map.AbsEntry; +import cn.hutool.core.map.MapUtil; + +import java.util.AbstractMap; +import java.util.AbstractSet; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * 将行的键作为主键的{@link Table}实现
+ * 此结构为: 行=(列=值) + * + * @param 行类型 + * @param 列类型 + * @param 值类型 + * @author Guava, Looly + * @since 5.7.23 + */ +public class RowKeyTable extends AbsTable { + + final Map> raw; + /** + * 列的Map创建器,用于定义Table中Value对应Map类型 + */ + final Builder> columnBuilder; + + //region 构造 + + /** + * 构造 + */ + public RowKeyTable() { + this(new HashMap<>()); + } + + /** + * 构造 + * + * @param isLinked 是否有序,有序则使用{@link java.util.LinkedHashMap}作为原始Map + * @since 5.8.0 + */ + public RowKeyTable(boolean isLinked) { + this(MapUtil.newHashMap(isLinked), () -> MapUtil.newHashMap(isLinked)); + } + + /** + * 构造 + * + * @param raw 原始Map + */ + public RowKeyTable(Map> raw) { + this(raw, HashMap::new); + } + + /** + * 构造 + * + * @param raw 原始Map + * @param columnMapBuilder 列的map创建器 + */ + public RowKeyTable(Map> raw, Builder> columnMapBuilder) { + this.raw = raw; + this.columnBuilder = null == columnMapBuilder ? HashMap::new : columnMapBuilder; + } + //endregion + + @Override + public Map> rowMap() { + return raw; + } + + @Override + public V put(R rowKey, C columnKey, V value) { + return raw.computeIfAbsent(rowKey, (key) -> columnBuilder.build()).put(columnKey, value); + } + + @Override + public V remove(R rowKey, C columnKey) { + final Map map = getRow(rowKey); + if (null == map) { + return null; + } + final V value = map.remove(columnKey); + if (map.isEmpty()) { + raw.remove(rowKey); + } + return value; + } + + @Override + public boolean isEmpty() { + return raw.isEmpty(); + } + + @Override + public void clear() { + this.raw.clear(); + } + + @Override + public boolean containsColumn(C columnKey) { + if (columnKey == null) { + return false; + } + for (Map map : raw.values()) { + if (null != map && map.containsKey(columnKey)) { + return true; + } + } + return false; + } + + //region columnMap + @Override + public Map> columnMap() { + Map> result = columnMap; + return (result == null) ? columnMap = new ColumnMap() : result; + } + + private Map> columnMap; + + private class ColumnMap extends AbstractMap> { + @Override + public Set>> entrySet() { + return new ColumnMapEntrySet(); + } + } + + private class ColumnMapEntrySet extends AbstractSet>> { + private final Set columnKeySet = columnKeySet(); + + @Override + public Iterator>> iterator() { + return new TransIter<>(columnKeySet.iterator(), + c -> MapUtil.entry(c, getColumn(c))); + } + + @Override + public int size() { + return columnKeySet.size(); + } + } + //endregion + + + //region columnKeySet + @Override + public Set columnKeySet() { + Set result = columnKeySet; + return (result == null) ? columnKeySet = new ColumnKeySet() : result; + } + + private Set columnKeySet; + + private class ColumnKeySet extends AbstractSet { + + @Override + public Iterator iterator() { + return new ColumnKeyIterator(); + } + + @Override + public int size() { + return IterUtil.size(iterator()); + } + } + + private class ColumnKeyIterator extends ComputeIter { + final Map seen = columnBuilder.build(); + final Iterator> mapIterator = raw.values().iterator(); + Iterator> entryIterator = IterUtil.empty(); + + @Override + protected C computeNext() { + while (true) { + if (entryIterator.hasNext()) { + Map.Entry entry = entryIterator.next(); + if (!seen.containsKey(entry.getKey())) { + seen.put(entry.getKey(), entry.getValue()); + return entry.getKey(); + } + } else if (mapIterator.hasNext()) { + entryIterator = mapIterator.next().entrySet().iterator(); + } else { + return null; + } + } + } + } + //endregion + + //region getColumn + @Override + public List columnKeys() { + final Collection> values = this.raw.values(); + final List result = new ArrayList<>(values.size() * 16); + for (Map map : values) { + map.forEach((key, value)->{result.add(key);}); + } + return result; + } + + @Override + public Map getColumn(C columnKey) { + return new Column(columnKey); + } + + private class Column extends AbstractMap { + final C columnKey; + + Column(C columnKey) { + this.columnKey = columnKey; + } + + @Override + public Set> entrySet() { + return new EntrySet(); + } + + private class EntrySet extends AbstractSet> { + + @Override + public Iterator> iterator() { + return new EntrySetIterator(); + } + + @Override + public int size() { + int size = 0; + for (Map map : raw.values()) { + if (map.containsKey(columnKey)) { + size++; + } + } + return size; + } + } + + private class EntrySetIterator extends ComputeIter> { + final Iterator>> iterator = raw.entrySet().iterator(); + + @Override + protected Entry computeNext() { + while (iterator.hasNext()) { + final Entry> entry = iterator.next(); + if (entry.getValue().containsKey(columnKey)) { + return new AbsEntry() { + @Override + public R getKey() { + return entry.getKey(); + } + + @Override + public V getValue() { + return entry.getValue().get(columnKey); + } + + @Override + public V setValue(V value) { + return entry.getValue().put(columnKey, value); + } + }; + } + } + return null; + } + } + } + //endregion +} diff --git a/src/main/java/cn/hutool/core/map/multi/SetValueMap.java b/src/main/java/cn/hutool/core/map/multi/SetValueMap.java new file mode 100644 index 0000000..d63055f --- /dev/null +++ b/src/main/java/cn/hutool/core/map/multi/SetValueMap.java @@ -0,0 +1,73 @@ +package cn.hutool.core.map.multi; + +import java.util.Collection; +import java.util.HashMap; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; + +/** + * 值作为集合Set(LinkedHashSet)的Map实现,通过调用putValue可以在相同key时加入多个值,多个值用集合表示 + * + * @author looly + * + * @param 键类型 + * @param 值类型 + * @since 4.3.3 + */ +public class SetValueMap extends AbsCollValueMap> { + private static final long serialVersionUID = 6044017508487827899L; + + // ------------------------------------------------------------------------- Constructor start + /** + * 构造 + */ + public SetValueMap() { + this(DEFAULT_INITIAL_CAPACITY); + } + + /** + * 构造 + * + * @param initialCapacity 初始大小 + */ + public SetValueMap(int initialCapacity) { + this(initialCapacity, DEFAULT_LOAD_FACTOR); + } + + /** + * 构造 + * + * @param m Map + */ + public SetValueMap(Map> m) { + this(DEFAULT_LOAD_FACTOR, m); + } + + /** + * 构造 + * + * @param loadFactor 加载因子 + * @param m Map + */ + public SetValueMap(float loadFactor, Map> m) { + this(m.size(), loadFactor); + this.putAllValues(m); + } + + /** + * 构造 + * + * @param initialCapacity 初始大小 + * @param loadFactor 加载因子 + */ + public SetValueMap(int initialCapacity, float loadFactor) { + super(new HashMap<>(initialCapacity, loadFactor)); + } + // ------------------------------------------------------------------------- Constructor end + + @Override + protected Set createCollection() { + return new LinkedHashSet<>(DEFAULT_COLLECTION_INITIAL_CAPACITY); + } +} diff --git a/src/main/java/cn/hutool/core/map/multi/Table.java b/src/main/java/cn/hutool/core/map/multi/Table.java new file mode 100644 index 0000000..f6f0119 --- /dev/null +++ b/src/main/java/cn/hutool/core/map/multi/Table.java @@ -0,0 +1,281 @@ +package cn.hutool.core.map.multi; + +import cn.hutool.core.collection.ListUtil; +import cn.hutool.core.lang.Opt; +import cn.hutool.core.lang.func.Consumer3; +import cn.hutool.core.map.MapUtil; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * 表格数据结构定义
+ * 此结构类似于Guava的Table接口,使用两个键映射到一个值,类似于表格结构。 + * + * @param 行键类型 + * @param 列键类型 + * @param 值类型 + * @since 5.7.23 + */ +public interface Table extends Iterable> { + + /** + * 是否包含指定行列的映射
+ * 行和列任意一个不存在都会返回{@code false},如果行和列都存在,值为{@code null},也会返回{@code true} + * + * @param rowKey 行键 + * @param columnKey 列键 + * @return 是否包含映射 + */ + default boolean contains(R rowKey, C columnKey) { + return Opt.ofNullable(getRow(rowKey)).map((map) -> map.containsKey(columnKey)).get(); + } + + //region Row + + /** + * 行是否存在 + * + * @param rowKey 行键 + * @return 行是否存在 + */ + default boolean containsRow(R rowKey) { + return Opt.ofNullable(rowMap()).map((map) -> map.containsKey(rowKey)).get(); + } + + /** + * 获取行 + * + * @param rowKey 行键 + * @return 行映射,返回的键为列键,值为表格的值 + */ + default Map getRow(R rowKey) { + return Opt.ofNullable(rowMap()).map((map) -> map.get(rowKey)).get(); + } + + /** + * 返回所有行的key,行的key不可重复 + * + * @return 行键 + */ + default Set rowKeySet() { + return Opt.ofNullable(rowMap()).map(Map::keySet).get(); + } + + /** + * 返回行列对应的Map + * + * @return map,键为行键,值为列和值的对应map + */ + Map> rowMap(); + //endregion + + //region Column + + /** + * 列是否存在 + * + * @param columnKey 列键 + * @return 列是否存在 + */ + default boolean containsColumn(C columnKey) { + return Opt.ofNullable(columnMap()).map((map) -> map.containsKey(columnKey)).get(); + } + + /** + * 获取列 + * + * @param columnKey 列键 + * @return 列映射,返回的键为行键,值为表格的值 + */ + default Map getColumn(C columnKey) { + return Opt.ofNullable(columnMap()).map((map) -> map.get(columnKey)).get(); + } + + /** + * 返回所有列的key,列的key不可重复 + * + * @return 列set + */ + default Set columnKeySet() { + return Opt.ofNullable(columnMap()).map(Map::keySet).get(); + } + + /** + * 返回所有列的key,列的key如果实现Map是可重复key,则返回对应不去重的List。 + * + * @return 列set + * @since 5.8.0 + */ + default List columnKeys() { + final Map> columnMap = columnMap(); + if(MapUtil.isEmpty(columnMap)){ + return ListUtil.empty(); + } + + final List result = new ArrayList<>(columnMap.size()); + for (Map.Entry> cMapEntry : columnMap.entrySet()) { + result.add(cMapEntry.getKey()); + } + return result; + } + + /** + * 返回列-行对应的map + * + * @return map,键为列键,值为行和值的对应map + */ + Map> columnMap(); + //endregion + + //region value + + /** + * 指定值是否存在 + * + * @param value 值 + * @return 值 + */ + default boolean containsValue(V value){ + final Collection> rows = Opt.ofNullable(rowMap()).map(Map::values).get(); + if(null != rows){ + for (Map row : rows) { + if (row.containsValue(value)) { + return true; + } + } + } + return false; + } + + /** + * 获取指定值 + * + * @param rowKey 行键 + * @param columnKey 列键 + * @return 值,如果值不存在,返回{@code null} + */ + default V get(R rowKey, C columnKey) { + return Opt.ofNullable(getRow(rowKey)).map((map) -> map.get(columnKey)).get(); + } + + /** + * 所有行列值的集合 + * + * @return 值的集合 + */ + Collection values(); + //endregion + + /** + * 所有单元格集合 + * + * @return 单元格集合 + */ + Set> cellSet(); + + /** + * 为表格指定行列赋值,如果不存在,创建之,存在则替换之,返回原值 + * + * @param rowKey 行键 + * @param columnKey 列键 + * @param value 值 + * @return 原值,不存在返回{@code null} + */ + V put(R rowKey, C columnKey, V value); + + /** + * 批量加入 + * + * @param table 其他table + */ + default void putAll(Table table){ + if (null != table) { + for (Cell cell : table.cellSet()) { + put(cell.getRowKey(), cell.getColumnKey(), cell.getValue()); + } + } + } + + /** + * 移除指定值 + * + * @param rowKey 行键 + * @param columnKey 列键 + * @return 移除的值,如果值不存在,返回{@code null} + */ + V remove(R rowKey, C columnKey); + + /** + * 表格是否为空 + * + * @return 是否为空 + */ + boolean isEmpty(); + + /** + * 表格大小,一般为单元格的个数 + * + * @return 表格大小 + */ + default int size(){ + final Map> rowMap = rowMap(); + if(MapUtil.isEmpty(rowMap)){ + return 0; + } + int size = 0; + for (Map map : rowMap.values()) { + size += map.size(); + } + return size; + } + + /** + * 清空表格 + */ + void clear(); + + /** + * 遍历表格的单元格,处理值 + * + * @param consumer 单元格值处理器 + */ + default void forEach(Consumer3 consumer) { + for (Cell cell : this) { + consumer.accept(cell.getRowKey(), cell.getColumnKey(), cell.getValue()); + } + } + + /** + * 单元格,用于表示一个单元格的行、列和值 + * + * @param 行键类型 + * @param 列键类型 + * @param 值类型 + */ + interface Cell { + /** + * 获取行键 + * + * @return 行键 + */ + R getRowKey(); + + /** + * 获取列键 + * + * @return 列键 + */ + C getColumnKey(); + + /** + * 获取值 + * + * @return 值 + */ + V getValue(); + } +} diff --git a/src/main/java/cn/hutool/core/map/multi/package-info.java b/src/main/java/cn/hutool/core/map/multi/package-info.java new file mode 100644 index 0000000..2bf5b68 --- /dev/null +++ b/src/main/java/cn/hutool/core/map/multi/package-info.java @@ -0,0 +1,7 @@ +/** + * 多参数类型的Map实现,包括集合类型值的Map和Table + * + * @author looly + * + */ +package cn.hutool.core.map.multi; diff --git a/src/main/java/cn/hutool/core/map/package-info.java b/src/main/java/cn/hutool/core/map/package-info.java new file mode 100644 index 0000000..4316fcb --- /dev/null +++ b/src/main/java/cn/hutool/core/map/package-info.java @@ -0,0 +1,7 @@ +/** + * Map相关封装,提供特殊Map实现以及Map工具MapUtil + * + * @author looly + * + */ +package cn.hutool.core.map; \ No newline at end of file diff --git a/src/main/java/cn/hutool/core/math/Arrangement.java b/src/main/java/cn/hutool/core/math/Arrangement.java new file mode 100644 index 0000000..b0dc6fb --- /dev/null +++ b/src/main/java/cn/hutool/core/math/Arrangement.java @@ -0,0 +1,127 @@ +package cn.hutool.core.math; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.NumberUtil; + +/** + * 排列A(n, m)
+ * 排列组合相关类 参考:http://cgs1999.iteye.com/blog/2327664 + * + * @author looly + * @since 4.0.7 + */ +public class Arrangement implements Serializable { + private static final long serialVersionUID = 1L; + + private final String[] datas; + + /** + * 构造 + * + * @param datas 用于排列的数据 + */ + public Arrangement(String[] datas) { + this.datas = datas; + } + + /** + * 计算排列数,即A(n, n) = n! + * + * @param n 总数 + * @return 排列数 + */ + public static long count(int n) { + return count(n, n); + } + + /** + * 计算排列数,即A(n, m) = n!/(n-m)! + * + * @param n 总数 + * @param m 选择的个数 + * @return 排列数 + */ + public static long count(int n, int m) { + if (n == m) { + return NumberUtil.factorial(n); + } + return (n > m) ? NumberUtil.factorial(n, n - m) : 0; + } + + /** + * 计算排列总数,即A(n, 1) + A(n, 2) + A(n, 3)... + * + * @param n 总数 + * @return 排列数 + */ + public static long countAll(int n) { + long total = 0; + for (int i = 1; i <= n; i++) { + total += count(n, i); + } + return total; + } + + /** + * 全排列选择(列表全部参与排列) + * + * @return 所有排列列表 + */ + public List select() { + return select(this.datas.length); + } + + /** + * 排列选择(从列表中选择m个排列) + * + * @param m 选择个数 + * @return 所有排列列表 + */ + public List select(int m) { + final List result = new ArrayList<>((int) count(this.datas.length, m)); + select(this.datas, new String[m], 0, result); + return result; + } + + /** + * 排列所有组合,即A(n, 1) + A(n, 2) + A(n, 3)... + * + * @return 全排列结果 + */ + public List selectAll() { + final List result = new ArrayList<>((int) countAll(this.datas.length)); + for (int i = 1; i <= this.datas.length; i++) { + result.addAll(select(i)); + } + return result; + } + + /** + * 排列选择
+ * 排列方式为先从数据数组中取出一个元素,再把剩余的元素作为新的基数,依次列推,直到选择到足够的元素 + * + * @param datas 选择的基数 + * @param resultList 前面(resultIndex-1)个的排列结果 + * @param resultIndex 选择索引,从0开始 + * @param result 最终结果 + */ + private void select(String[] datas, String[] resultList, int resultIndex, List result) { + if (resultIndex >= resultList.length) { // 全部选择完时,输出排列结果 + if (!result.contains(resultList)) { + result.add(Arrays.copyOf(resultList, resultList.length)); + } + return; + } + + // 递归选择下一个 + for (int i = 0; i < datas.length; i++) { + resultList[resultIndex] = datas[i]; + select(ArrayUtil.remove(datas, i), resultList, resultIndex + 1, result); + } + } +} diff --git a/src/main/java/cn/hutool/core/math/BitStatusUtil.java b/src/main/java/cn/hutool/core/math/BitStatusUtil.java new file mode 100644 index 0000000..001f47d --- /dev/null +++ b/src/main/java/cn/hutool/core/math/BitStatusUtil.java @@ -0,0 +1,81 @@ +package cn.hutool.core.math; + +/** + * 通过位运算表示状态的工具类
+ * 参数必须是 `偶数` 且 `大于等于0`! + * + * 工具实现见博客:https://blog.starxg.com/2020/11/bit-status/ + * + * @author huangxingguang,senssic + * @since 5.6.6 + */ +public class BitStatusUtil { + + /** + * 增加状态 + * + * @param states 原状态 + * @param stat 要添加的状态 + * @return 新的状态值 + */ + public static int add(int states, int stat) { + check(states, stat); + return states | stat; + } + + /** + * 判断是否含有状态 + * + * @param states 原状态 + * @param stat 要判断的状态 + * @return true:有 + */ + public static boolean has(int states, int stat) { + check(states, stat); + return (states & stat) == stat; + } + + /** + * 删除一个状态 + * + * @param states 原状态 + * @param stat 要删除的状态 + * @return 新的状态值 + */ + public static int remove(int states, int stat) { + check(states, stat); + if (has(states, stat)) { + return states ^ stat; + } + return states; + } + + /** + * 清空状态就是0 + * + * @return 0 + */ + public static int clear() { + return 0; + } + + /** + * 检查 + *
    + *
  • 必须大于0
  • + *
  • 必须为偶数
  • + *
+ * + * @param args 被检查的状态 + */ + private static void check(int... args) { + for (int arg : args) { + if (arg < 0) { + throw new IllegalArgumentException(arg + " 必须大于等于0"); + } + if ((arg & 1) == 1) { + throw new IllegalArgumentException(arg + " 不是偶数"); + } + } + } +} diff --git a/src/main/java/cn/hutool/core/math/Calculator.java b/src/main/java/cn/hutool/core/math/Calculator.java new file mode 100644 index 0000000..3dc6524 --- /dev/null +++ b/src/main/java/cn/hutool/core/math/Calculator.java @@ -0,0 +1,201 @@ +package cn.hutool.core.math; + +import cn.hutool.core.util.NumberUtil; +import cn.hutool.core.util.StrUtil; + +import java.math.BigDecimal; +import java.util.Collections; +import java.util.Stack; + +/** + * 数学表达式计算工具类
+ * 见:https://github.com/dromara/hutool/issues/1090#issuecomment-693750140 + * + * @author trainliang, looly + * @since 5.4.3 + */ +public class Calculator { + private final Stack postfixStack = new Stack<>();// 后缀式栈 + private final int[] operatPriority = new int[]{0, 3, 2, 1, -1, 1, 0, 2};// 运用运算符ASCII码-40做索引的运算符优先级 + + /** + * 计算表达式的值 + * + * @param expression 表达式 + * @return 计算结果 + */ + public static double conversion(String expression) { + return (new Calculator()).calculate(expression); + } + + /** + * 按照给定的表达式计算 + * + * @param expression 要计算的表达式例如:5+12*(3+5)/7 + * @return 计算结果 + */ + public double calculate(String expression) { + prepare(transform(expression)); + + Stack resultStack = new Stack<>(); + Collections.reverse(postfixStack);// 将后缀式栈反转 + String firstValue, secondValue, currentOp;// 参与计算的第一个值,第二个值和算术运算符 + while (!postfixStack.isEmpty()) { + currentOp = postfixStack.pop(); + if (!isOperator(currentOp.charAt(0))) {// 如果不是运算符则存入操作数栈中 + currentOp = currentOp.replace("~", "-"); + resultStack.push(currentOp); + } else {// 如果是运算符则从操作数栈中取两个值和该数值一起参与运算 + secondValue = resultStack.pop(); + firstValue = resultStack.pop(); + + // 将负数标记符改为负号 + firstValue = firstValue.replace("~", "-"); + secondValue = secondValue.replace("~", "-"); + + BigDecimal tempResult = calculate(firstValue, secondValue, currentOp.charAt(0)); + resultStack.push(tempResult.toString()); + } + } + return Double.parseDouble(resultStack.pop()); + } + + /** + * 数据准备阶段将表达式转换成为后缀式栈 + * + * @param expression 表达式 + */ + private void prepare(String expression) { + final Stack opStack = new Stack<>(); + opStack.push(',');// 运算符放入栈底元素逗号,此符号优先级最低 + char[] arr = expression.toCharArray(); + int currentIndex = 0;// 当前字符的位置 + int count = 0;// 上次算术运算符到本次算术运算符的字符的长度便于或者之间的数值 + char currentOp, peekOp;// 当前操作符和栈顶操作符 + for (int i = 0; i < arr.length; i++) { + currentOp = arr[i]; + if (isOperator(currentOp)) {// 如果当前字符是运算符 + if (count > 0) { + postfixStack.push(new String(arr, currentIndex, count));// 取两个运算符之间的数字 + } + peekOp = opStack.peek(); + if (currentOp == ')') {// 遇到反括号则将运算符栈中的元素移除到后缀式栈中直到遇到左括号 + while (opStack.peek() != '(') { + postfixStack.push(String.valueOf(opStack.pop())); + } + opStack.pop(); + } else { + while (currentOp != '(' && peekOp != ',' && compare(currentOp, peekOp)) { + postfixStack.push(String.valueOf(opStack.pop())); + peekOp = opStack.peek(); + } + opStack.push(currentOp); + } + count = 0; + currentIndex = i + 1; + } else { + count++; + } + } + if (count > 1 || (count == 1 && !isOperator(arr[currentIndex]))) {// 最后一个字符不是括号或者其他运算符的则加入后缀式栈中 + postfixStack.push(new String(arr, currentIndex, count)); + } + + while (opStack.peek() != ',') { + postfixStack.push(String.valueOf(opStack.pop()));// 将操作符栈中的剩余的元素添加到后缀式栈中 + } + } + + /** + * 判断是否为算术符号 + * + * @param c 字符 + * @return 是否为算术符号 + */ + private boolean isOperator(char c) { + return c == '+' || c == '-' || c == '*' || c == '/' || c == '(' || c == ')' || c == '%'; + } + + /** + * 利用ASCII码-40做下标去算术符号优先级 + * + * @param cur 下标 + * @param peek peek + * @return 优先级,如果cur高或相等,返回true,否则false + */ + private boolean compare(char cur, char peek) {// 如果是peek优先级高于cur,返回true,默认都是peek优先级要低 + final int offset = 40; + if(cur == '%'){ + // %优先级最高 + cur = 47; + } + if(peek == '%'){ + // %优先级最高 + peek = 47; + } + + return operatPriority[peek - offset] >= operatPriority[cur - offset]; + } + + /** + * 按照给定的算术运算符做计算 + * + * @param firstValue 第一个值 + * @param secondValue 第二个值 + * @param currentOp 算数符,只支持'+'、'-'、'*'、'/'、'%' + * @return 结果 + */ + private BigDecimal calculate(String firstValue, String secondValue, char currentOp) { + BigDecimal result; + switch (currentOp) { + case '+': + result = NumberUtil.add(firstValue, secondValue); + break; + case '-': + result = NumberUtil.sub(firstValue, secondValue); + break; + case '*': + result = NumberUtil.mul(firstValue, secondValue); + break; + case '/': + result = NumberUtil.div(firstValue, secondValue); + break; + case '%': + result = NumberUtil.toBigDecimal(firstValue).remainder(NumberUtil.toBigDecimal(secondValue)); + break; + default: + throw new IllegalStateException("Unexpected value: " + currentOp); + } + return result; + } + + /** + * 将表达式中负数的符号更改 + * + * @param expression 例如-2+-1*(-3E-2)-(-1) 被转为 ~2+~1*(~3E~2)-(~1) + * @return 转换后的字符串 + */ + private static String transform(String expression) { + expression = StrUtil.cleanBlank(expression); + expression = StrUtil.removeSuffix(expression, "="); + final char[] arr = expression.toCharArray(); + for (int i = 0; i < arr.length; i++) { + if (arr[i] == '-') { + if (i == 0) { + arr[i] = '~'; + } else { + char c = arr[i - 1]; + if (c == '+' || c == '-' || c == '*' || c == '/' || c == '(' || c == 'E' || c == 'e') { + arr[i] = '~'; + } + } + } + } + if (arr[0] == '~' && (arr.length > 1 && arr[1] == '(')) { + arr[0] = '-'; + return "0" + new String(arr); + } else { + return new String(arr); + } + } +} diff --git a/src/main/java/cn/hutool/core/math/Combination.java b/src/main/java/cn/hutool/core/math/Combination.java new file mode 100644 index 0000000..7da8d3b --- /dev/null +++ b/src/main/java/cn/hutool/core/math/Combination.java @@ -0,0 +1,107 @@ +package cn.hutool.core.math; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import cn.hutool.core.util.NumberUtil; +import cn.hutool.core.util.StrUtil; + +/** + * 组合,即C(n, m)
+ * 排列组合相关类 参考:http://cgs1999.iteye.com/blog/2327664 + * + * @author looly + * @since 4.0.6 + */ +public class Combination implements Serializable { + private static final long serialVersionUID = 1L; + + private final String[] datas; + + /** + * 组合,即C(n, m)
+ * 排列组合相关类 参考:http://cgs1999.iteye.com/blog/2327664 + * + * @param datas 用于组合的数据 + */ + public Combination(String[] datas) { + this.datas = datas; + } + + /** + * 计算组合数,即C(n, m) = n!/((n-m)! * m!) + * + * @param n 总数 + * @param m 选择的个数 + * @return 组合数 + */ + public static long count(int n, int m) { + if (0 == m || n == m) { + return 1; + } + return (n > m) ? NumberUtil.factorial(n, n - m) / NumberUtil.factorial(m) : 0; + } + + /** + * 计算组合总数,即C(n, 1) + C(n, 2) + C(n, 3)... + * + * @param n 总数 + * @return 组合数 + */ + public static long countAll(int n) { + if (n < 0 || n > 63) { + throw new IllegalArgumentException(StrUtil.format("countAll must have n >= 0 and n <= 63, but got n={}", n)); + } + return n == 63 ? Long.MAX_VALUE : (1L << n) - 1; + } + + /** + * 组合选择(从列表中选择m个组合) + * + * @param m 选择个数 + * @return 组合结果 + */ + public List select(int m) { + final List result = new ArrayList<>((int) count(this.datas.length, m)); + select(0, new String[m], 0, result); + return result; + } + + /** + * 全组合 + * + * @return 全排列结果 + */ + public List selectAll() { + final List result = new ArrayList<>((int) countAll(this.datas.length)); + for (int i = 1; i <= this.datas.length; i++) { + result.addAll(select(i)); + } + return result; + } + + /** + * 组合选择 + * + * @param dataIndex 待选开始索引 + * @param resultList 前面(resultIndex-1)个的组合结果 + * @param resultIndex 选择索引,从0开始 + * @param result 结果集 + */ + private void select(int dataIndex, String[] resultList, int resultIndex, List result) { + int resultLen = resultList.length; + int resultCount = resultIndex + 1; + if (resultCount > resultLen) { // 全部选择完时,输出组合结果 + result.add(Arrays.copyOf(resultList, resultList.length)); + return; + } + + // 递归选择下一个 + for (int i = dataIndex; i < datas.length + resultCount - resultLen; i++) { + resultList[resultIndex] = datas[i]; + select(i + 1, resultList, resultIndex + 1, result); + } + } +} diff --git a/src/main/java/cn/hutool/core/math/MathUtil.java b/src/main/java/cn/hutool/core/math/MathUtil.java new file mode 100644 index 0000000..c0bc63d --- /dev/null +++ b/src/main/java/cn/hutool/core/math/MathUtil.java @@ -0,0 +1,103 @@ +package cn.hutool.core.math; + +import java.util.List; + +/** + * 数学相关方法工具类
+ * 此工具类与{@link cn.hutool.core.util.NumberUtil}属于一类工具,NumberUtil偏向于简单数学计算的封装,MathUtil偏向复杂数学计算 + * + * @author looly + * @since 4.0.7 + */ +public class MathUtil { + + //--------------------------------------------------------------------------------------------- Arrangement + /** + * 计算排列数,即A(n, m) = n!/(n-m)! + * + * @param n 总数 + * @param m 选择的个数 + * @return 排列数 + */ + public static long arrangementCount(int n, int m) { + return Arrangement.count(n, m); + } + + /** + * 计算排列数,即A(n, n) = n! + * + * @param n 总数 + * @return 排列数 + */ + public static long arrangementCount(int n) { + return Arrangement.count(n); + } + + /** + * 排列选择(从列表中选择n个排列) + * + * @param datas 待选列表 + * @param m 选择个数 + * @return 所有排列列表 + */ + public static List arrangementSelect(String[] datas, int m) { + return new Arrangement(datas).select(m); + } + + /** + * 全排列选择(列表全部参与排列) + * + * @param datas 待选列表 + * @return 所有排列列表 + */ + public static List arrangementSelect(String[] datas) { + return new Arrangement(datas).select(); + } + + //--------------------------------------------------------------------------------------------- Combination + /** + * 计算组合数,即C(n, m) = n!/((n-m)! * m!) + * + * @param n 总数 + * @param m 选择的个数 + * @return 组合数 + */ + public static long combinationCount(int n, int m) { + return Combination.count(n, m); + } + + /** + * 组合选择(从列表中选择n个组合) + * + * @param datas 待选列表 + * @param m 选择个数 + * @return 所有组合列表 + */ + public static List combinationSelect(String[] datas, int m) { + return new Combination(datas).select(m); + } + + /** + * 金额元转换为分 + * + * @param yuan 金额,单位元 + * @return 金额,单位分 + * @since 5.7.11 + */ + public static long yuanToCent(double yuan) { + return new Money(yuan).getCent(); + } + + /** + * 金额分转换为元 + * + * @param cent 金额,单位分 + * @return 金额,单位元 + * @since 5.7.11 + */ + public static double centToYuan(long cent) { + long yuan = cent / 100; + int centPart = (int) (cent % 100); + return new Money(yuan, centPart).getAmount().doubleValue(); + } +} diff --git a/src/main/java/cn/hutool/core/math/Money.java b/src/main/java/cn/hutool/core/math/Money.java new file mode 100644 index 0000000..1583904 --- /dev/null +++ b/src/main/java/cn/hutool/core/math/Money.java @@ -0,0 +1,854 @@ +package cn.hutool.core.math; + +import cn.hutool.core.util.StrUtil; + +import java.io.File; +import java.io.Serializable; +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.Currency; + +/** + * 单币种货币类,处理货币算术、币种和取整。 + *

+ * 感谢提供此方法的用户:https://github.com/dromara/hutool/issues/605 + * + *

+ * 货币类中封装了货币金额和币种。目前金额在内部是long类型表示, + * 单位是所属币种的最小货币单位(对人民币是分)。 + * + *

+ * 目前,货币实现了以下主要功能:
+ *

    + *
  • 支持货币对象与double(float)/long(int)/String/BigDecimal之间相互转换。 + *
  • 货币类在运算中提供与JDK中的BigDecimal类似的运算接口, + * BigDecimal的运算接口支持任意指定精度的运算功能,能够支持各种 + * 可能的财务规则。 + *
  • 货币类在运算中也提供一组简单运算接口,使用这组运算接口,则在 + * 精度处理上使用缺省的处理规则。 + *
  • 推荐使用Money,不建议直接使用BigDecimal的原因之一在于, + * 使用BigDecimal,同样金额和币种的货币使用BigDecimal存在多种可能 + * 的表示,例如:new BigDecimal("10.5")与new BigDecimal("10.50") + * 不相等,因为scale不等。使得Money类,同样金额和币种的货币只有 + * 一种表示方式,new Money("10.5")和new Money("10.50")应该是相等的。 + *
  • 不推荐直接使用BigDecimal的另一原因在于, BigDecimal是Immutable, + * 一旦创建就不可更改,对BigDecimal进行任意运算都会生成一个新的 + * BigDecimal对象,因此对于大批量统计的性能不够满意。Money类是 + * mutable的,对大批量统计提供较好的支持。 + *
  • 提供基本的格式化功能。 + *
  • Money类中不包含与业务相关的统计功能和格式化功能。业务相关的功能 + * 建议使用utility类来实现。 + *
  • Money类实现了Serializable接口,支持作为远程调用的参数和返回值。 + *
  • Money类实现了equals和hashCode方法。 + *
+ * + * @author ddatsh + * @since 5.0.4 + */ + +public class Money implements Serializable, Comparable { + private static final long serialVersionUID = -1004117971993390293L; + + /** + * 缺省的币种代码,为CNY(人民币)。 + */ + public static final String DEFAULT_CURRENCY_CODE = "CNY"; + + /** + * 缺省的取整模式,为{@link RoundingMode#HALF_EVEN} + * (四舍五入,当小数为0.5时,则取最近的偶数)。 + */ + public static final RoundingMode DEFAULT_ROUNDING_MODE = RoundingMode.HALF_EVEN; + + /** + * 一组可能的元/分换算比例。 + * + *

+ * 此处,“分”是指货币的最小单位,“元”是货币的最常用单位, + * 不同的币种有不同的元/分换算比例,如人民币是100,而日元为1。 + */ + private static final int[] CENT_FACTORS = new int[]{1, 10, 100, 1000}; + + /** + * 金额,以分为单位。 + */ + private long cent; + + /** + * 币种。 + */ + private final Currency currency; + + // 构造器 ==================================================== + + /** + * 缺省构造器。 + * + *

+ * 创建一个具有缺省金额(0)和缺省币种的货币对象。 + */ + public Money() { + this(0); + } + + /** + * 构造器。 + * + *

+ * 创建一个具有金额{@code yuan}元{@code cent}分和缺省币种的货币对象。 + * + * @param yuan 金额元数,0的情况下表示元的部分从分中截取 + * @param cent 金额分数。 + */ + public Money(long yuan, int cent) { + this(yuan, cent, Currency.getInstance(DEFAULT_CURRENCY_CODE)); + } + + /** + * 构造器。 + * + *

+ * 创建一个具有金额{@code yuan}元{@code cent}分和指定币种的货币对象。 + * + * @param yuan 金额元数,0的情况下表示元的部分从分中截取 + * @param cent 金额分数。 + * @param currency 货币单位 + */ + public Money(long yuan, int cent, Currency currency) { + this.currency = currency; + + if(0 == yuan) { + this.cent = cent; + } else{ + this.cent = (yuan * getCentFactor()) + (cent % getCentFactor()); + } + } + + /** + * 构造器。 + * + *

+ * 创建一个具有金额{@code amount}元和缺省币种的货币对象。 + * + * @param amount 金额,以元为单位。 + */ + public Money(String amount) { + this(amount, Currency.getInstance(DEFAULT_CURRENCY_CODE)); + } + + /** + * 构造器。 + * + *

+ * 创建一个具有金额{@code amount}元和指定币种{@code currency}的货币对象。 + * + * @param amount 金额,以元为单位。 + * @param currency 币种。 + */ + public Money(String amount, Currency currency) { + this(new BigDecimal(amount), currency); + } + + /** + * 构造器。 + * + *

+ * 创建一个具有金额{@code amount}元和指定币种{@code currency}的货币对象。 + * 如果金额不能转换为整数分,则使用指定的取整模式{@code roundingMode}取整。 + * + * @param amount 金额,以元为单位。 + * @param currency 币种。 + * @param roundingMode 取整模式。 + */ + public Money(String amount, Currency currency, RoundingMode roundingMode) { + this(new BigDecimal(amount), currency, roundingMode); + } + + /** + * 构造器。 + * + *

+ * 创建一个具有参数{@code amount}指定金额和缺省币种的货币对象。 + * 如果金额不能转换为整数分,则使用四舍五入方式取整。 + * + *

+ * 注意:由于double类型运算中存在误差,使用四舍五入方式取整的 + * 结果并不确定,因此,应尽量避免使用double类型创建货币类型。 + * 例: + * {@code + * assertEquals(999, Math.round(9.995 * 100)); + * assertEquals(1000, Math.round(999.5)); + * money = new Money((9.995)); + * assertEquals(999, money.getCent()); + * money = new Money(10.005); + * assertEquals(1001, money.getCent()); + * } + * + * @param amount 金额,以元为单位。 + */ + public Money(double amount) { + this(amount, Currency.getInstance(DEFAULT_CURRENCY_CODE)); + } + + /** + * 构造器。 + * + *

+ * 创建一个具有金额{@code amount}和指定币种的货币对象。 + * 如果金额不能转换为整数分,则使用四舍五入方式取整。 + * + *

+ * 注意:由于double类型运算中存在误差,使用四舍五入方式取整的 + * 结果并不确定,因此,应尽量避免使用double类型创建货币类型。 + * 例: + * {@code + * assertEquals(999, Math.round(9.995 * 100)); + * assertEquals(1000, Math.round(999.5)); + * money = new Money((9.995)); + * assertEquals(999, money.getCent()); + * money = new Money(10.005); + * assertEquals(1001, money.getCent()); + * } + * + * @param amount 金额,以元为单位。 + * @param currency 币种。 + */ + public Money(double amount, Currency currency) { + this.currency = currency; + this.cent = Math.round(amount * getCentFactor()); + } + + /** + * 构造器。 + * + *

+ * 创建一个具有金额{@code amount}和缺省币种的货币对象。 + * 如果金额不能转换为整数分,则使用缺省取整模式{@code DEFAULT_ROUNDING_MODE}取整。 + * + * @param amount 金额,以元为单位。 + */ + public Money(BigDecimal amount) { + this(amount, Currency.getInstance(DEFAULT_CURRENCY_CODE)); + } + + /** + * 构造器。 + * + *

+ * 创建一个具有参数{@code amount}指定金额和缺省币种的货币对象。 + * 如果金额不能转换为整数分,则使用指定的取整模式{@code roundingMode}取整。 + * + * @param amount 金额,以元为单位。 + * @param roundingMode 取整模式 + */ + public Money(BigDecimal amount, RoundingMode roundingMode) { + this(amount, Currency.getInstance(DEFAULT_CURRENCY_CODE), roundingMode); + } + + /** + * 构造器。 + * + *

+ * 创建一个具有金额{@code amount}和指定币种的货币对象。 + * 如果金额不能转换为整数分,则使用缺省的取整模式{@code DEFAULT_ROUNDING_MODE}进行取整。 + * + * @param amount 金额,以元为单位。 + * @param currency 币种 + */ + public Money(BigDecimal amount, Currency currency) { + this(amount, currency, DEFAULT_ROUNDING_MODE); + } + + /** + * 构造器。 + * + *

+ * 创建一个具有金额{@code amount}和指定币种的货币对象。 + * 如果金额不能转换为整数分,则使用指定的取整模式{@code roundingMode}取整。 + * + * @param amount 金额,以元为单位。 + * @param currency 币种。 + * @param roundingMode 取整模式。 + */ + public Money(BigDecimal amount, Currency currency, RoundingMode roundingMode) { + this.currency = currency; + this.cent = rounding(amount.movePointRight(currency.getDefaultFractionDigits()), + roundingMode); + } + + // Bean方法 ==================================================== + + /** + * 获取本货币对象代表的金额数。 + * + * @return 金额数,以元为单位。 + */ + public BigDecimal getAmount() { + return BigDecimal.valueOf(cent, currency.getDefaultFractionDigits()); + } + + /** + * 设置本货币对象代表的金额数。 + * + * @param amount 金额数,以元为单位。 + */ + public void setAmount(BigDecimal amount) { + if (amount != null) { + cent = rounding(amount.movePointRight(2), DEFAULT_ROUNDING_MODE); + } + } + + /** + * 获取本货币对象代表的金额数。 + * + * @return 金额数,以分为单位。 + */ + public long getCent() { + return cent; + } + + /** + * 获取本货币对象代表的币种。 + * + * @return 本货币对象所代表的币种。 + */ + public Currency getCurrency() { + return currency; + } + + /** + * 获取本货币币种的元/分换算比率。 + * + * @return 本货币币种的元/分换算比率。 + */ + public int getCentFactor() { + return CENT_FACTORS[currency.getDefaultFractionDigits()]; + } + + // 基本对象方法 =================================================== + + /** + * 判断本货币对象与另一对象是否相等。 + *

+ * 货币对象与另一对象相等的充分必要条件是:
+ *

    + *
  • 另一对象也属货币对象类。 + *
  • 金额相同。 + *
  • 币种相同。 + *
+ * + * @param other 待比较的另一对象。 + * @return {@code true}表示相等,{@code false}表示不相等。 + */ + @Override + public boolean equals(Object other) { + return (other instanceof Money) && equals((Money) other); + } + + /** + * 判断本货币对象与另一货币对象是否相等。 + *

+ * 货币对象与另一货币对象相等的充分必要条件是:
+ *

    + *
  • 金额相同。 + *
  • 币种相同。 + *
+ * + * @param other 待比较的另一货币对象。 + * @return {@code true}表示相等,{@code false}表示不相等。 + */ + public boolean equals(Money other) { + return currency.equals(other.currency) && (cent == other.cent); + } + + /** + * 计算本货币对象的杂凑值。 + * + * @return 本货币对象的杂凑值。 + */ + @Override + public int hashCode() { + return (int) (cent ^ (cent >>> 32)); + } + + /** + * 货币比较。 + * + *

+ * 比较本货币对象与另一货币对象的大小。 + * 如果待比较的两个货币对象的币种不同,则抛出{@code java.lang.IllegalArgumentException}。 + * 如果本货币对象的金额少于待比较货币对象,则返回-1。 + * 如果本货币对象的金额等于待比较货币对象,则返回0。 + * 如果本货币对象的金额大于待比较货币对象,则返回1。 + * + * @param other 另一对象。 + * @return -1表示小于,0表示等于,1表示大于。 + * @throws IllegalArgumentException 待比较货币对象与本货币对象的币种不同。 + */ + @Override + public int compareTo(Money other) { + assertSameCurrencyAs(other); + return Long.compare(cent, other.cent); + } + + /** + * 货币比较。 + * + *

+ * 判断本货币对象是否大于另一货币对象。 + * 如果待比较的两个货币对象的币种不同,则抛出{@code java.lang.IllegalArgumentException}。 + * 如果本货币对象的金额大于待比较货币对象,则返回true,否则返回false。 + * + * @param other 另一对象。 + * @return true表示大于,false表示不大于(小于等于)。 + * @throws IllegalArgumentException 待比较货币对象与本货币对象的币种不同。 + */ + public boolean greaterThan(Money other) { + return compareTo(other) > 0; + } + + // 货币算术 ========================================== + + /** + * 货币加法。 + * + *

+ * 如果两货币币种相同,则返回一个新的相同币种的货币对象,其金额为 + * 两货币对象金额之和,本货币对象的值不变。 + * 如果两货币对象币种不同,抛出{@code java.lang.IllegalArgumentException}。 + * + * @param other 作为加数的货币对象。 + * @return 相加后的结果。 + * @throws IllegalArgumentException 如果本货币对象与另一货币对象币种不同。 + */ + public Money add(Money other) { + assertSameCurrencyAs(other); + + return newMoneyWithSameCurrency(cent + other.cent); + } + + /** + * 货币累加。 + * + *

+ * 如果两货币币种相同,则本货币对象的金额等于两货币对象金额之和,并返回本货币对象的引用。 + * 如果两货币对象币种不同,抛出{@code java.lang.IllegalArgumentException}。 + * + * @param other 作为加数的货币对象。 + * @return 累加后的本货币对象。 + * @throws IllegalArgumentException 如果本货币对象与另一货币对象币种不同。 + */ + public Money addTo(Money other) { + assertSameCurrencyAs(other); + + this.cent += other.cent; + + return this; + } + + /** + * 货币减法。 + * + *

+ * 如果两货币币种相同,则返回一个新的相同币种的货币对象,其金额为 + * 本货币对象的金额减去参数货币对象的金额。本货币对象的值不变。 + * 如果两货币币种不同,抛出{@code java.lang.IllegalArgumentException}。 + * + * @param other 作为减数的货币对象。 + * @return 相减后的结果。 + * @throws IllegalArgumentException 如果本货币对象与另一货币对象币种不同。 + */ + public Money subtract(Money other) { + assertSameCurrencyAs(other); + + return newMoneyWithSameCurrency(cent - other.cent); + } + + /** + * 货币累减。 + * + *

+ * 如果两货币币种相同,则本货币对象的金额等于两货币对象金额之差,并返回本货币对象的引用。 + * 如果两货币币种不同,抛出{@code java.lang.IllegalArgumentException}。 + * + * @param other 作为减数的货币对象。 + * @return 累减后的本货币对象。 + * @throws IllegalArgumentException 如果本货币对象与另一货币对象币种不同。 + */ + public Money subtractFrom(Money other) { + assertSameCurrencyAs(other); + + this.cent -= other.cent; + + return this; + } + + /** + * 货币乘法。 + * + *

+ * 返回一个新的货币对象,币种与本货币对象相同,金额为本货币对象的金额乘以乘数。 + * 本货币对象的值不变。 + * + * @param val 乘数 + * @return 乘法后的结果。 + */ + public Money multiply(long val) { + return newMoneyWithSameCurrency(cent * val); + } + + /** + * 货币累乘。 + * + *

+ * 本货币对象金额乘以乘数,并返回本货币对象。 + * + * @param val 乘数 + * @return 累乘后的本货币对象。 + */ + public Money multiplyBy(long val) { + this.cent *= val; + + return this; + } + + /** + * 货币乘法。 + * + *

+ * 返回一个新的货币对象,币种与本货币对象相同,金额为本货币对象的金额乘以乘数。 + * 本货币对象的值不变。如果相乘后的金额不能转换为整数分,则四舍五入。 + * + * @param val 乘数 + * @return 相乘后的结果。 + */ + public Money multiply(double val) { + return newMoneyWithSameCurrency(Math.round(cent * val)); + } + + /** + * 货币累乘。 + * + *

+ * 本货币对象金额乘以乘数,并返回本货币对象。 + * 如果相乘后的金额不能转换为整数分,则使用四舍五入。 + * + * @param val 乘数 + * @return 累乘后的本货币对象。 + */ + public Money multiplyBy(double val) { + this.cent = Math.round(this.cent * val); + + return this; + } + + /** + * 货币乘法。 + * + *

+ * 返回一个新的货币对象,币种与本货币对象相同,金额为本货币对象的金额乘以乘数。 + * 本货币对象的值不变。如果相乘后的金额不能转换为整数分,使用缺省的取整模式 + * {@code DEFUALT_ROUNDING_MODE}进行取整。 + * + * @param val 乘数 + * @return 相乘后的结果。 + */ + public Money multiply(BigDecimal val) { + return multiply(val, DEFAULT_ROUNDING_MODE); + } + + /** + * 货币累乘。 + * + *

+ * 本货币对象金额乘以乘数,并返回本货币对象。 + * 如果相乘后的金额不能转换为整数分,使用缺省的取整方式 + * {@code DEFUALT_ROUNDING_MODE}进行取整。 + * + * @param val 乘数 + * @return 累乘后的结果。 + */ + public Money multiplyBy(BigDecimal val) { + return multiplyBy(val, DEFAULT_ROUNDING_MODE); + } + + /** + * 货币乘法。 + * + *

+ * 返回一个新的货币对象,币种与本货币对象相同,金额为本货币对象的金额乘以乘数。 + * 本货币对象的值不变。如果相乘后的金额不能转换为整数分,使用指定的取整方式 + * {@code roundingMode}进行取整。 + * + * @param val 乘数 + * @param roundingMode 取整方式 + * @return 相乘后的结果。 + */ + public Money multiply(BigDecimal val, RoundingMode roundingMode) { + BigDecimal newCent = BigDecimal.valueOf(cent).multiply(val); + + return newMoneyWithSameCurrency(rounding(newCent, roundingMode)); + } + + /** + * 货币累乘。 + * + *

+ * 本货币对象金额乘以乘数,并返回本货币对象。 + * 如果相乘后的金额不能转换为整数分,使用指定的取整方式 + * {@code roundingMode}进行取整。 + * + * @param val 乘数 + * @param roundingMode 取整方式 + * @return 累乘后的结果。 + */ + public Money multiplyBy(BigDecimal val, RoundingMode roundingMode) { + BigDecimal newCent = BigDecimal.valueOf(cent).multiply(val); + + this.cent = rounding(newCent, roundingMode); + + return this; + } + + /** + * 货币除法。 + * + *

+ * 返回一个新的货币对象,币种与本货币对象相同,金额为本货币对象的金额除以除数。 + * 本货币对象的值不变。如果相除后的金额不能转换为整数分,使用四舍五入方式取整。 + * + * @param val 除数 + * @return 相除后的结果。 + */ + public Money divide(double val) { + return newMoneyWithSameCurrency(Math.round(cent / val)); + } + + /** + * 货币累除。 + * + *

+ * 本货币对象金额除以除数,并返回本货币对象。 + * 如果相除后的金额不能转换为整数分,使用四舍五入方式取整。 + * + * @param val 除数 + * @return 累除后的结果。 + */ + public Money divideBy(double val) { + this.cent = Math.round(this.cent / val); + + return this; + } + + /** + * 货币除法。 + * + *

+ * 返回一个新的货币对象,币种与本货币对象相同,金额为本货币对象的金额除以除数。 + * 本货币对象的值不变。如果相除后的金额不能转换为整数分,使用缺省的取整模式 + * {@code DEFAULT_ROUNDING_MODE}进行取整。 + * + * @param val 除数 + * @return 相除后的结果。 + */ + public Money divide(BigDecimal val) { + return divide(val, DEFAULT_ROUNDING_MODE); + } + + /** + * 货币除法。 + * + *

+ * 返回一个新的货币对象,币种与本货币对象相同,金额为本货币对象的金额除以除数。 + * 本货币对象的值不变。如果相除后的金额不能转换为整数分,使用指定的取整模式 + * {@code roundingMode}进行取整。 + * + * @param val 除数 + * @param roundingMode 取整 + * @return 相除后的结果。 + */ + public Money divide(BigDecimal val, RoundingMode roundingMode) { + BigDecimal newCent = BigDecimal.valueOf(cent).divide(val, roundingMode); + + return newMoneyWithSameCurrency(newCent.longValue()); + } + + /** + * 货币累除。 + * + *

+ * 本货币对象金额除以除数,并返回本货币对象。 + * 如果相除后的金额不能转换为整数分,使用缺省的取整模式 + * {@code DEFAULT_ROUNDING_MODE}进行取整。 + * + * @param val 除数 + * @return 累除后的结果。 + */ + public Money divideBy(BigDecimal val) { + return divideBy(val, DEFAULT_ROUNDING_MODE); + } + + /** + * 货币累除。 + * + *

+ * 本货币对象金额除以除数,并返回本货币对象。 + * 如果相除后的金额不能转换为整数分,使用指定的取整模式 + * {@code roundingMode}进行取整。 + * + * @param val 除数 + * @param roundingMode 保留小数方式 + * @return 累除后的结果。 + */ + public Money divideBy(BigDecimal val, RoundingMode roundingMode) { + BigDecimal newCent = BigDecimal.valueOf(cent).divide(val, roundingMode); + + this.cent = newCent.longValue(); + + return this; + } + + /** + * 货币分配。 + * + *

+ * 将本货币对象尽可能平均分配成{@code targets}份。 + * 如果不能平均分配尽,则将零头放到开始的若干份中。分配 + * 运算能够确保不会丢失金额零头。 + * + * @param targets 待分配的份数 + * @return 货币对象数组,数组的长度与分配份数相同,数组元素 + * 从大到小排列,所有货币对象的金额最多只相差1分。 + */ + public Money[] allocate(int targets) { + Money[] results = new Money[targets]; + + Money lowResult = newMoneyWithSameCurrency(cent / targets); + Money highResult = newMoneyWithSameCurrency(lowResult.cent + 1); + + int remainder = (int) cent % targets; + + for (int i = 0; i < remainder; i++) { + results[i] = highResult; + } + + for (int i = remainder; i < targets; i++) { + results[i] = lowResult; + } + + return results; + } + + /** + * 货币分配。 + * + *

+ * 将本货币对象按照规定的比例分配成若干份。分配所剩的零头 + * 从第一份开始顺序分配。分配运算确保不会丢失金额零头。 + * + * @param ratios 分配比例数组,每一个比例是一个长整型,代表 + * 相对于总数的相对数。 + * @return 货币对象数组,数组的长度与分配比例数组的长度相同。 + */ + public Money[] allocate(long[] ratios) { + Money[] results = new Money[ratios.length]; + + long total = 0; + + for (long element : ratios) { + total += element; + } + + long remainder = cent; + + for (int i = 0; i < results.length; i++) { + results[i] = newMoneyWithSameCurrency((cent * ratios[i]) / total); + remainder -= results[i].cent; + } + + for (int i = 0; i < remainder; i++) { + results[i].cent++; + } + + return results; + } + + // 格式化方法 ================================================= + + /** + * 生成本对象的缺省字符串表示 + */ + @Override + public String toString() { + return getAmount().toString(); + } + + // 内部方法 =================================================== + + /** + * 断言本货币对象与另一货币对象是否具有相同的币种。 + * + *

+ * 如果本货币对象与另一货币对象具有相同的币种,则方法返回。 + * 否则抛出运行时异常{@code java.lang.IllegalArgumentException}。 + * + * @param other 另一货币对象 + * @throws IllegalArgumentException 如果本货币对象与另一货币对象币种不同。 + */ + protected void assertSameCurrencyAs(Money other) { + if (!currency.equals(other.currency)) { + throw new IllegalArgumentException("Money math currency mismatch."); + } + } + + /** + * 对BigDecimal型的值按指定取整方式取整。 + * + * @param val 待取整的BigDecimal值 + * @param roundingMode 取整方式 + * @return 取整后的long型值 + */ + protected long rounding(BigDecimal val, RoundingMode roundingMode) { + return val.setScale(0, roundingMode).longValue(); + } + + /** + * 创建一个币种相同,具有指定金额的货币对象。 + * + * @param cent 金额,以分为单位 + * @return 一个新建的币种相同,具有指定金额的货币对象 + */ + protected Money newMoneyWithSameCurrency(long cent) { + Money money = new Money(0, currency); + + money.cent = cent; + + return money; + } + + // 调试方式 ================================================== + + /** + * 生成本对象内部变量的字符串表示,用于调试。 + * + * @return 本对象内部变量的字符串表示。 + */ + public String dump() { + return StrUtil.builder() + .append("cent = ") + .append(this.cent) + .append(File.separatorChar) + .append("currency = ") + .append(this.currency) + .toString(); + } + + /** + * 设置货币的分值。 + * + * @param cent 分值 + */ + public void setCent(long cent) { + this.cent = cent; + } +} diff --git a/src/main/java/cn/hutool/core/math/package-info.java b/src/main/java/cn/hutool/core/math/package-info.java new file mode 100644 index 0000000..18b5d86 --- /dev/null +++ b/src/main/java/cn/hutool/core/math/package-info.java @@ -0,0 +1,7 @@ +/** + * 提供数学计算相关封装,包括排列组合等,入口为MathUtil + * + * @author looly + * + */ +package cn.hutool.core.math; \ No newline at end of file diff --git a/src/main/java/cn/hutool/core/net/DefaultTrustManager.java b/src/main/java/cn/hutool/core/net/DefaultTrustManager.java new file mode 100644 index 0000000..8a96105 --- /dev/null +++ b/src/main/java/cn/hutool/core/net/DefaultTrustManager.java @@ -0,0 +1,51 @@ +package cn.hutool.core.net; + +import javax.net.ssl.SSLEngine; +import javax.net.ssl.X509ExtendedTrustManager; +import java.net.Socket; +import java.security.cert.X509Certificate; + +/** + * 默认信任管理器,默认信任所有客户端和服务端证书
+ * 继承{@link X509ExtendedTrustManager}的原因见:https://blog.csdn.net/ghaohao/article/details/79454913 + * + * @author Looly + * @since 5.5.7 + */ +public class DefaultTrustManager extends X509ExtendedTrustManager { + + /** + * 默认的全局单例默认信任管理器,默认信任所有客户端和服务端证书 + * @since 5.7.8 + */ + public static DefaultTrustManager INSTANCE = new DefaultTrustManager(); + + @Override + public X509Certificate[] getAcceptedIssuers() { + return new X509Certificate[0]; + } + + @Override + public void checkClientTrusted(X509Certificate[] chain, String authType) { + } + + @Override + public void checkServerTrusted(X509Certificate[] chain, String authType) { + } + + @Override + public void checkClientTrusted(X509Certificate[] x509Certificates, String s, Socket socket) { + } + + @Override + public void checkServerTrusted(X509Certificate[] x509Certificates, String s, Socket socket) { + } + + @Override + public void checkClientTrusted(X509Certificate[] x509Certificates, String s, SSLEngine sslEngine) { + } + + @Override + public void checkServerTrusted(X509Certificate[] x509Certificates, String s, SSLEngine sslEngine) { + } +} diff --git a/src/main/java/cn/hutool/core/net/FormUrlencoded.java b/src/main/java/cn/hutool/core/net/FormUrlencoded.java new file mode 100644 index 0000000..1b41f02 --- /dev/null +++ b/src/main/java/cn/hutool/core/net/FormUrlencoded.java @@ -0,0 +1,19 @@ +package cn.hutool.core.net; + +import cn.hutool.core.codec.PercentCodec; + +/** + * application/x-www-form-urlencoded,遵循W3C HTML Form content types规范,如空格须转+,+须被编码
+ * 规范见:https://url.spec.whatwg.org/#urlencoded-serializing + * + * @since 5.7.16 + */ +public class FormUrlencoded { + + /** + * query中的value,默认除"-", "_", ".", "*"外都编码
+ * 这个类似于JDK提供的{@link java.net.URLEncoder} + */ + public static final PercentCodec ALL = PercentCodec.of(RFC3986.UNRESERVED) + .removeSafe('~').addSafe('*').setEncodeSpaceAsPlus(true); +} diff --git a/src/main/java/cn/hutool/core/net/Ipv4Util.java b/src/main/java/cn/hutool/core/net/Ipv4Util.java new file mode 100644 index 0000000..1d466b4 --- /dev/null +++ b/src/main/java/cn/hutool/core/net/Ipv4Util.java @@ -0,0 +1,410 @@ +package cn.hutool.core.net; + +import cn.hutool.core.collection.ListUtil; +import cn.hutool.core.convert.Convert; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.lang.PatternPool; +import cn.hutool.core.lang.Validator; +import cn.hutool.core.util.CharUtil; +import cn.hutool.core.util.StrUtil; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.regex.Matcher; + +/** + * IPV4地址工具类 + * + *

pr自:https://gitee.com/loolly/hutool/pulls/161

+ * + * @author ZhuKun + * @since 5.4.1 + */ +public class Ipv4Util { + + public static final String LOCAL_IP = "127.0.0.1"; + + /** + * IP段的分割符 + */ + public static final String IP_SPLIT_MARK = "-"; + + /** + * IP与掩码的分割符 + */ + public static final String IP_MASK_SPLIT_MARK = StrUtil.SLASH; + + /** + * 最大掩码位 + */ + public static final int IP_MASK_MAX = 32; + + /** + * 格式化IP段 + * + * @param ip IP地址 + * @param mask 掩码 + * @return 返回xxx.xxx.xxx.xxx/mask的格式 + */ + public static String formatIpBlock(String ip, String mask) { + return ip + IP_MASK_SPLIT_MARK + getMaskBitByMask(mask); + } + + /** + * 智能转换IP地址集合 + * + * @param ipRange IP段,支持X.X.X.X-X.X.X.X或X.X.X.X/X + * @param isAll true:全量地址,false:可用地址;仅在ipRange为X.X.X.X/X时才生效 + * @return IP集 + */ + public static List list(String ipRange, boolean isAll) { + if (ipRange.contains(IP_SPLIT_MARK)) { + // X.X.X.X-X.X.X.X + final String[] range = StrUtil.splitToArray(ipRange, IP_SPLIT_MARK); + return list(range[0], range[1]); + } else if (ipRange.contains(IP_MASK_SPLIT_MARK)) { + // X.X.X.X/X + final String[] param = StrUtil.splitToArray(ipRange, IP_MASK_SPLIT_MARK); + return list(param[0], Integer.parseInt(param[1]), isAll); + } else { + return ListUtil.toList(ipRange); + } + } + + /** + * 根据IP地址、子网掩码获取IP地址区间 + * + * @param ip IP地址 + * @param maskBit 掩码位,例如24、32 + * @param isAll true:全量地址,false:可用地址 + * @return 区间地址 + */ + public static List list(String ip, int maskBit, boolean isAll) { + if (maskBit == IP_MASK_MAX) { + final List list = new ArrayList<>(); + if (isAll) { + list.add(ip); + } + return list; + } + + String startIp = getBeginIpStr(ip, maskBit); + String endIp = getEndIpStr(ip, maskBit); + if (isAll) { + return list(startIp, endIp); + } + + int lastDotIndex = startIp.lastIndexOf(CharUtil.DOT) + 1; + startIp = StrUtil.subPre(startIp, lastDotIndex) + + (Integer.parseInt(Objects.requireNonNull(StrUtil.subSuf(startIp, lastDotIndex))) + 1); + lastDotIndex = endIp.lastIndexOf(CharUtil.DOT) + 1; + endIp = StrUtil.subPre(endIp, lastDotIndex) + + (Integer.parseInt(Objects.requireNonNull(StrUtil.subSuf(endIp, lastDotIndex))) - 1); + return list(startIp, endIp); + } + + /** + * 得到IP地址区间 + * + * @param ipFrom 开始IP + * @param ipTo 结束IP + * @return 区间地址 + */ + public static List list(String ipFrom, String ipTo) { + // 确定ip数量 + final int count = countByIpRange(ipFrom, ipTo); + final int[] from = Convert.convert(int[].class, StrUtil.splitToArray(ipFrom, CharUtil.DOT)); + final int[] to = Convert.convert(int[].class, StrUtil.splitToArray(ipTo, CharUtil.DOT)); + + final List ips = new ArrayList<>(count); + // 是否是循环的第一个值 + boolean aIsStart = true, bIsStart = true, cIsStart = true; + // 是否是循环的最后一个值 + boolean aIsEnd, bIsEnd, cIsEnd; + // 循环的结束值 + int aEnd = to[0], bEnd, cEnd, dEnd; + for (int a = from[0]; a <= aEnd; a++) { + aIsEnd = (a == aEnd); + // 本次循环的结束结束值 + bEnd = aIsEnd ? to[1] : 255; + for (int b = (aIsStart ? from[1] : 0); b <= bEnd; b++) { + // 在上一个循环是最后值的基础上进行判断 + bIsEnd = aIsEnd && (b == bEnd); + cEnd = bIsEnd ? to[2] : 255; + for (int c = (bIsStart ? from[2] : 0); c <= cEnd; c++) { + // 在之前循环是最后值的基础上进行判断 + cIsEnd = bIsEnd && (c == cEnd); + dEnd = cIsEnd ? to[3] : 255; + for (int d = (cIsStart ? from[3] : 0); d <= dEnd; d++) { + ips.add(a + "." + b + "." + c + "." + d); + } + cIsStart = false; + } + bIsStart = false; + } + aIsStart = false; + } + return ips; + } + + /** + * 根据long值获取ip v4地址:xx.xx.xx.xx + * + * @param longIP IP的long表示形式 + * @return IP V4 地址 + */ + public static String longToIpv4(long longIP) { + final StringBuilder sb = StrUtil.builder(); + // 直接右移24位 + sb.append(longIP >> 24 & 0xFF); + sb.append(CharUtil.DOT); + // 将高8位置0,然后右移16位 + sb.append(longIP >> 16 & 0xFF); + sb.append(CharUtil.DOT); + sb.append(longIP >> 8 & 0xFF); + sb.append(CharUtil.DOT); + sb.append(longIP & 0xFF); + return sb.toString(); + } + + /** + * 根据ip地址(xxx.xxx.xxx.xxx)计算出long型的数据 + * 方法别名:inet_aton + * + * @param strIP IP V4 地址 + * @return long值 + */ + public static long ipv4ToLong(String strIP) { + final Matcher matcher = PatternPool.IPV4.matcher(strIP); + if (matcher.matches()) { + return matchAddress(matcher); + } +// Validator.validateIpv4(strIP, "Invalid IPv4 address!"); +// final long[] ip = Convert.convert(long[].class, StrUtil.split(strIP, CharUtil.DOT)); +// return (ip[0] << 24) + (ip[1] << 16) + (ip[2] << 8) + ip[3]; + throw new IllegalArgumentException("Invalid IPv4 address!"); + } + + /** + * 根据ip地址(xxx.xxx.xxx.xxx)计算出long型的数据, 如果格式不正确返回 defaultValue + * @param strIP IP V4 地址 + * @param defaultValue 默认值 + * @return long值 + */ + public static long ipv4ToLong(String strIP, long defaultValue) { + return Validator.isIpv4(strIP) ? ipv4ToLong(strIP) : defaultValue; + } + + /** + * 根据 ip/掩码位 计算IP段的起始IP(字符串型) + * 方法别名:inet_ntoa + * + * @param ip 给定的IP,如218.240.38.69 + * @param maskBit 给定的掩码位,如30 + * @return 起始IP的字符串表示 + */ + public static String getBeginIpStr(String ip, int maskBit) { + return longToIpv4(getBeginIpLong(ip, maskBit)); + } + + /** + * 根据 ip/掩码位 计算IP段的起始IP(Long型) + * + * @param ip 给定的IP,如218.240.38.69 + * @param maskBit 给定的掩码位,如30 + * @return 起始IP的长整型表示 + */ + public static Long getBeginIpLong(String ip, int maskBit) { + return ipv4ToLong(ip) & ipv4ToLong(getMaskByMaskBit(maskBit)); + } + + /** + * 根据 ip/掩码位 计算IP段的终止IP(字符串型) + * + * @param ip 给定的IP,如218.240.38.69 + * @param maskBit 给定的掩码位,如30 + * @return 终止IP的字符串表示 + */ + public static String getEndIpStr(String ip, int maskBit) { + return longToIpv4(getEndIpLong(ip, maskBit)); + } + + /** + * 根据子网掩码转换为掩码位 + * + * @param mask 掩码的点分十进制表示,例如 255.255.255.0 + * @return 掩码位,例如 24 + * @throws IllegalArgumentException 子网掩码非法 + */ + public static int getMaskBitByMask(String mask) { + Integer maskBit = MaskBit.getMaskBit(mask); + if (maskBit == null) { + throw new IllegalArgumentException("Invalid netmask " + mask); + } + return maskBit; + } + + /** + * 计算子网大小 + * + * @param maskBit 掩码位 + * @param isAll true:全量地址,false:可用地址 + * @return 地址总数 + */ + public static int countByMaskBit(int maskBit, boolean isAll) { + //如果是可用地址的情况,掩码位小于等于0或大于等于32,则可用地址为0 + if ((!isAll) && (maskBit <= 0 || maskBit >= 32)) { + return 0; + } + + final int count = (int) Math.pow(2, 32 - maskBit); + return isAll ? count : count - 2; + } + + /** + * 根据掩码位获取掩码 + * + * @param maskBit 掩码位 + * @return 掩码 + */ + public static String getMaskByMaskBit(int maskBit) { + return MaskBit.get(maskBit); + } + + /** + * 根据开始IP与结束IP计算掩码 + * + * @param fromIp 开始IP + * @param toIp 结束IP + * @return 掩码x.x.x.x + */ + public static String getMaskByIpRange(String fromIp, String toIp) { + long toIpLong = ipv4ToLong(toIp); + long fromIpLong = ipv4ToLong(fromIp); + Assert.isTrue(fromIpLong < toIpLong, "to IP must be greater than from IP!"); + + String[] fromIpSplit = StrUtil.splitToArray(fromIp, CharUtil.DOT); + String[] toIpSplit = StrUtil.splitToArray(toIp, CharUtil.DOT); + StringBuilder mask = new StringBuilder(); + for (int i = 0; i < toIpSplit.length; i++) { + mask.append(255 - Integer.parseInt(toIpSplit[i]) + Integer.parseInt(fromIpSplit[i])).append(CharUtil.DOT); + } + return mask.substring(0, mask.length() - 1); + } + + /** + * 计算IP区间有多少个IP + * + * @param fromIp 开始IP + * @param toIp 结束IP + * @return IP数量 + */ + public static int countByIpRange(String fromIp, String toIp) { + long toIpLong = ipv4ToLong(toIp); + long fromIpLong = ipv4ToLong(fromIp); + if (fromIpLong > toIpLong) { + throw new IllegalArgumentException("to IP must be greater than from IP!"); + } + int count = 1; + int[] fromIpSplit = StrUtil.split(fromIp, CharUtil.DOT).stream().mapToInt(Integer::parseInt).toArray(); + int[] toIpSplit = StrUtil.split(toIp, CharUtil.DOT).stream().mapToInt(Integer::parseInt).toArray(); + for (int i = fromIpSplit.length - 1; i >= 0; i--) { + count += (toIpSplit[i] - fromIpSplit[i]) * Math.pow(256, fromIpSplit.length - i - 1); + } + return count; + } + + /** + * 判断掩码是否合法 + * + * @param mask 掩码的点分十进制表示,例如 255.255.255.0 + * @return true:掩码合法;false:掩码不合法 + */ + public static boolean isMaskValid(String mask) { + return MaskBit.getMaskBit(mask) != null; + } + + /** + * 判断掩码位是否合法 + * + * @param maskBit 掩码位,例如 24 + * @return true:掩码位合法;false:掩码位不合法 + */ + public static boolean isMaskBitValid(int maskBit) { + return MaskBit.get(maskBit) != null; + } + + /** + * 判定是否为内网IPv4
+ * 私有IP: + *
+	 * A类 10.0.0.0-10.255.255.255
+	 * B类 172.16.0.0-172.31.255.255
+	 * C类 192.168.0.0-192.168.255.255
+	 * 
+ * 当然,还有127这个网段是环回地址 + * + * @param ipAddress IP地址 + * @return 是否为内网IP + * @since 5.7.18 + */ + public static boolean isInnerIP(String ipAddress) { + boolean isInnerIp; + long ipNum = ipv4ToLong(ipAddress); + + long aBegin = ipv4ToLong("10.0.0.0"); + long aEnd = ipv4ToLong("10.255.255.255"); + + long bBegin = ipv4ToLong("172.16.0.0"); + long bEnd = ipv4ToLong("172.31.255.255"); + + long cBegin = ipv4ToLong("192.168.0.0"); + long cEnd = ipv4ToLong("192.168.255.255"); + + isInnerIp = isInner(ipNum, aBegin, aEnd) || isInner(ipNum, bBegin, bEnd) || isInner(ipNum, cBegin, cEnd) || LOCAL_IP.equals(ipAddress); + return isInnerIp; + } + + //-------------------------------------------------------------------------------- Private method start + + /** + * 根据 ip/掩码位 计算IP段的终止IP(Long型) + * 注:此接口返回负数,请使用转成字符串后再转Long型 + * + * @param ip 给定的IP,如218.240.38.69 + * @param maskBit 给定的掩码位,如30 + * @return 终止IP的长整型表示 + */ + public static Long getEndIpLong(String ip, int maskBit) { + return getBeginIpLong(ip, maskBit) + + ~ipv4ToLong(getMaskByMaskBit(maskBit)); + } + + /** + * 将匹配到的Ipv4地址的4个分组分别处理 + * + * @param matcher 匹配到的Ipv4正则 + * @return ipv4对应long + */ + private static long matchAddress(Matcher matcher) { + long addr = 0; + for (int i = 1; i <= 4; ++i) { + addr |= Long.parseLong(matcher.group(i)) << 8 * (4 - i); + } + return addr; + } + + /** + * 指定IP的long是否在指定范围内 + * + * @param userIp 用户IP + * @param begin 开始IP + * @param end 结束IP + * @return 是否在范围内 + */ + private static boolean isInner(long userIp, long begin, long end) { + return (userIp >= begin) && (userIp <= end); + } + //-------------------------------------------------------------------------------- Private method end +} diff --git a/src/main/java/cn/hutool/core/net/MaskBit.java b/src/main/java/cn/hutool/core/net/MaskBit.java new file mode 100644 index 0000000..f153845 --- /dev/null +++ b/src/main/java/cn/hutool/core/net/MaskBit.java @@ -0,0 +1,76 @@ +package cn.hutool.core.net; + +import cn.hutool.core.map.BiMap; +import java.util.HashMap; + +/** + * 掩码位和掩码之间的Map对应 + * + * @since 5.4.1 + * @author looly + */ +public class MaskBit { + + /** + * 掩码位与掩码的点分十进制的双向对应关系 + */ + private static final BiMap MASK_BIT_MAP; + static { + MASK_BIT_MAP = new BiMap<>(new HashMap<>(32)); + MASK_BIT_MAP.put(1, "128.0.0.0"); + MASK_BIT_MAP.put(2, "192.0.0.0"); + MASK_BIT_MAP.put(3, "224.0.0.0"); + MASK_BIT_MAP.put(4, "240.0.0.0"); + MASK_BIT_MAP.put(5, "248.0.0.0"); + MASK_BIT_MAP.put(6, "252.0.0.0"); + MASK_BIT_MAP.put(7, "254.0.0.0"); + MASK_BIT_MAP.put(8, "255.0.0.0"); + MASK_BIT_MAP.put(9, "255.128.0.0"); + MASK_BIT_MAP.put(10, "255.192.0.0"); + MASK_BIT_MAP.put(11, "255.224.0.0"); + MASK_BIT_MAP.put(12, "255.240.0.0"); + MASK_BIT_MAP.put(13, "255.248.0.0"); + MASK_BIT_MAP.put(14, "255.252.0.0"); + MASK_BIT_MAP.put(15, "255.254.0.0"); + MASK_BIT_MAP.put(16, "255.255.0.0"); + MASK_BIT_MAP.put(17, "255.255.128.0"); + MASK_BIT_MAP.put(18, "255.255.192.0"); + MASK_BIT_MAP.put(19, "255.255.224.0"); + MASK_BIT_MAP.put(20, "255.255.240.0"); + MASK_BIT_MAP.put(21, "255.255.248.0"); + MASK_BIT_MAP.put(22, "255.255.252.0"); + MASK_BIT_MAP.put(23, "255.255.254.0"); + MASK_BIT_MAP.put(24, "255.255.255.0"); + MASK_BIT_MAP.put(25, "255.255.255.128"); + MASK_BIT_MAP.put(26, "255.255.255.192"); + MASK_BIT_MAP.put(27, "255.255.255.224"); + MASK_BIT_MAP.put(28, "255.255.255.240"); + MASK_BIT_MAP.put(29, "255.255.255.248"); + MASK_BIT_MAP.put(30, "255.255.255.252"); + MASK_BIT_MAP.put(31, "255.255.255.254"); + MASK_BIT_MAP.put(32, "255.255.255.255"); + } + + /** + * 根据掩码位获取掩码 + * + * @param maskBit 掩码位 + * @return 掩码 + */ + public static String get(int maskBit) { + return MASK_BIT_MAP.get(maskBit); + } + + /** + * 根据掩码获取掩码位 + * + * @param mask 掩码的点分十进制表示,如 255.255.255.0 + * + * @return 掩码位,如 24;如果掩码不合法,则返回null + * @since 5.6.5 + */ + public static Integer getMaskBit(String mask) { + return MASK_BIT_MAP.getKey(mask); + } + +} diff --git a/src/main/java/cn/hutool/core/net/NetUtil.java b/src/main/java/cn/hutool/core/net/NetUtil.java new file mode 100644 index 0000000..0d4ecd9 --- /dev/null +++ b/src/main/java/cn/hutool/core/net/NetUtil.java @@ -0,0 +1,833 @@ +package cn.hutool.core.net; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.exceptions.UtilException; +import cn.hutool.core.io.IORuntimeException; +import cn.hutool.core.io.IoUtil; +import cn.hutool.core.lang.Filter; +import cn.hutool.core.util.RandomUtil; +import cn.hutool.core.util.StrUtil; + +import java.io.IOException; +import java.io.OutputStream; +import java.math.BigInteger; +import java.net.*; +import java.nio.ByteBuffer; +import java.nio.channels.SocketChannel; +import java.util.*; + +/** + * 网络相关工具 + * + * @author xiaoleilu + */ +public class NetUtil { + + public final static String LOCAL_IP = Ipv4Util.LOCAL_IP; + + public static String localhostName; + + /** + * 默认最小端口,1024 + */ + public static final int PORT_RANGE_MIN = 1024; + /** + * 默认最大端口,65535 + */ + public static final int PORT_RANGE_MAX = 0xFFFF; + + /** + * 根据long值获取ip v4地址 + * + * @param longIP IP的long表示形式 + * @return IP V4 地址 + * @see Ipv4Util#longToIpv4(long) + */ + public static String longToIpv4(long longIP) { + return Ipv4Util.longToIpv4(longIP); + } + + /** + * 根据ip地址计算出long型的数据 + * + * @param strIP IP V4 地址 + * @return long值 + * @see Ipv4Util#ipv4ToLong(String) + */ + public static long ipv4ToLong(String strIP) { + return Ipv4Util.ipv4ToLong(strIP); + } + + /** + * 将IPv6地址字符串转为大整数 + * + * @param ipv6Str 字符串 + * @return 大整数, 如发生异常返回 null + * @since 5.5.7 + * @deprecated 拼写错误,请使用{@link #ipv6ToBigInteger(String)} + */ + @Deprecated + public static BigInteger ipv6ToBitInteger(String ipv6Str) { + return ipv6ToBigInteger(ipv6Str); + } + + /** + * 将IPv6地址字符串转为大整数 + * + * @param ipv6Str 字符串 + * @return 大整数, 如发生异常返回 null + * @since 5.5.7 + */ + public static BigInteger ipv6ToBigInteger(String ipv6Str) { + try { + InetAddress address = InetAddress.getByName(ipv6Str); + if (address instanceof Inet6Address) { + return new BigInteger(1, address.getAddress()); + } + } catch (UnknownHostException ignore) { + } + return null; + } + + /** + * 将大整数转换成ipv6字符串 + * + * @param bigInteger 大整数 + * @return IPv6字符串, 如发生异常返回 null + * @since 5.5.7 + */ + public static String bigIntegerToIPv6(BigInteger bigInteger) { + try { + return InetAddress.getByAddress(bigInteger.toByteArray()).toString().substring(1); + } catch (UnknownHostException ignore) { + return null; + } + } + + /** + * 检测本地端口可用性
+ * 来自org.springframework.util.SocketUtils + * + * @param port 被检测的端口 + * @return 是否可用 + */ + public static boolean isUsableLocalPort(int port) { + if (!isValidPort(port)) { + // 给定的IP未在指定端口范围中 + return false; + } + + // issue#765@Github, 某些绑定非127.0.0.1的端口无法被检测到 + try (ServerSocket ss = new ServerSocket(port)) { + ss.setReuseAddress(true); + } catch (IOException ignored) { + return false; + } + + try (DatagramSocket ds = new DatagramSocket(port)) { + ds.setReuseAddress(true); + } catch (IOException ignored) { + return false; + } + + return true; + } + + /** + * 是否为有效的端口
+ * 此方法并不检查端口是否被占用 + * + * @param port 端口号 + * @return 是否有效 + */ + public static boolean isValidPort(int port) { + // 有效端口是0~65535 + return port >= 0 && port <= PORT_RANGE_MAX; + } + + /** + * 查找1024~65535范围内的可用端口
+ * 此方法只检测给定范围内的随机一个端口,检测65535-1024次
+ * 来自org.springframework.util.SocketUtils + * + * @return 可用的端口 + * @since 4.5.4 + */ + public static int getUsableLocalPort() { + return getUsableLocalPort(PORT_RANGE_MIN); + } + + /** + * 查找指定范围内的可用端口,最大值为65535
+ * 此方法只检测给定范围内的随机一个端口,检测65535-minPort次
+ * 来自org.springframework.util.SocketUtils + * + * @param minPort 端口最小值(包含) + * @return 可用的端口 + * @since 4.5.4 + */ + public static int getUsableLocalPort(int minPort) { + return getUsableLocalPort(minPort, PORT_RANGE_MAX); + } + + /** + * 查找指定范围内的可用端口
+ * 此方法只检测给定范围内的随机一个端口,检测maxPort-minPort次
+ * 来自org.springframework.util.SocketUtils + * + * @param minPort 端口最小值(包含) + * @param maxPort 端口最大值(包含) + * @return 可用的端口 + * @since 4.5.4 + */ + public static int getUsableLocalPort(int minPort, int maxPort) { + final int maxPortExclude = maxPort + 1; + int randomPort; + for (int i = minPort; i < maxPortExclude; i++) { + randomPort = RandomUtil.randomInt(minPort, maxPortExclude); + if (isUsableLocalPort(randomPort)) { + return randomPort; + } + } + + throw new UtilException("Could not find an available port in the range [{}, {}] after {} attempts", minPort, maxPort, maxPort - minPort); + } + + /** + * 获取多个本地可用端口
+ * 来自org.springframework.util.SocketUtils + * + * @param numRequested 尝试次数 + * @param minPort 端口最小值(包含) + * @param maxPort 端口最大值(包含) + * @return 可用的端口 + * @since 4.5.4 + */ + public static TreeSet getUsableLocalPorts(int numRequested, int minPort, int maxPort) { + final TreeSet availablePorts = new TreeSet<>(); + int attemptCount = 0; + while ((++attemptCount <= numRequested + 100) && availablePorts.size() < numRequested) { + availablePorts.add(getUsableLocalPort(minPort, maxPort)); + } + + if (availablePorts.size() != numRequested) { + throw new UtilException("Could not find {} available ports in the range [{}, {}]", numRequested, minPort, maxPort); + } + + return availablePorts; + } + + /** + * 判定是否为内网IPv4
+ * 私有IP: + *
+	 * A类 10.0.0.0-10.255.255.255
+	 * B类 172.16.0.0-172.31.255.255
+	 * C类 192.168.0.0-192.168.255.255
+	 * 
+ * 当然,还有127这个网段是环回地址 + * + * @param ipAddress IP地址 + * @return 是否为内网IP + * @see Ipv4Util#isInnerIP(String) + */ + public static boolean isInnerIP(String ipAddress) { + return Ipv4Util.isInnerIP(ipAddress); + } + + /** + * 相对URL转换为绝对URL + * + * @param absoluteBasePath 基准路径,绝对 + * @param relativePath 相对路径 + * @return 绝对URL + */ + public static String toAbsoluteUrl(String absoluteBasePath, String relativePath) { + try { + URL absoluteUrl = new URL(absoluteBasePath); + return new URL(absoluteUrl, relativePath).toString(); + } catch (Exception e) { + throw new UtilException(e, "To absolute url [{}] base [{}] error!", relativePath, absoluteBasePath); + } + } + + /** + * 隐藏掉IP地址的最后一部分为 * 代替 + * + * @param ip IP地址 + * @return 隐藏部分后的IP + */ + public static String hideIpPart(String ip) { + return StrUtil.builder(ip.length()).append(ip, 0, ip.lastIndexOf(".") + 1).append("*").toString(); + } + + /** + * 隐藏掉IP地址的最后一部分为 * 代替 + * + * @param ip IP地址 + * @return 隐藏部分后的IP + */ + public static String hideIpPart(long ip) { + return hideIpPart(longToIpv4(ip)); + } + + /** + * 构建InetSocketAddress
+ * 当host中包含端口时(用“:”隔开),使用host中的端口,否则使用默认端口
+ * 给定host为空时使用本地host(127.0.0.1) + * + * @param host Host + * @param defaultPort 默认端口 + * @return InetSocketAddress + */ + public static InetSocketAddress buildInetSocketAddress(String host, int defaultPort) { + if (StrUtil.isBlank(host)) { + host = LOCAL_IP; + } + + String destHost; + int port; + int index = host.indexOf(":"); + if (index != -1) { + // host:port形式 + destHost = host.substring(0, index); + port = Integer.parseInt(host.substring(index + 1)); + } else { + destHost = host; + port = defaultPort; + } + + return new InetSocketAddress(destHost, port); + } + + /** + * 通过域名得到IP + * + * @param hostName HOST + * @return ip address or hostName if UnknownHostException + */ + public static String getIpByHost(String hostName) { + try { + return InetAddress.getByName(hostName).getHostAddress(); + } catch (UnknownHostException e) { + return hostName; + } + } + + /** + * 获取指定名称的网卡信息 + * + * @param name 网络接口名,例如Linux下默认是eth0 + * @return 网卡,未找到返回{@code null} + * @since 5.0.7 + */ + public static NetworkInterface getNetworkInterface(String name) { + Enumeration networkInterfaces; + try { + networkInterfaces = NetworkInterface.getNetworkInterfaces(); + } catch (SocketException e) { + return null; + } + + NetworkInterface netInterface; + while (networkInterfaces.hasMoreElements()) { + netInterface = networkInterfaces.nextElement(); + if (null != netInterface && name.equals(netInterface.getName())) { + return netInterface; + } + } + + return null; + } + + /** + * 获取本机所有网卡 + * + * @return 所有网卡,异常返回{@code null} + * @since 3.0.1 + */ + public static Collection getNetworkInterfaces() { + Enumeration networkInterfaces; + try { + networkInterfaces = NetworkInterface.getNetworkInterfaces(); + } catch (SocketException e) { + return null; + } + + return CollUtil.addAll(new ArrayList<>(), networkInterfaces); + } + + /** + * 获得本机的IPv4地址列表
+ * 返回的IP列表有序,按照系统设备顺序 + * + * @return IP地址列表 {@link LinkedHashSet} + */ + public static LinkedHashSet localIpv4s() { + final LinkedHashSet localAddressList = localAddressList(t -> t instanceof Inet4Address); + + return toIpList(localAddressList); + } + + /** + * 获得本机的IPv6地址列表
+ * 返回的IP列表有序,按照系统设备顺序 + * + * @return IP地址列表 {@link LinkedHashSet} + * @since 4.5.17 + */ + public static LinkedHashSet localIpv6s() { + final LinkedHashSet localAddressList = localAddressList(t -> t instanceof Inet6Address); + + return toIpList(localAddressList); + } + + /** + * 地址列表转换为IP地址列表 + * + * @param addressList 地址{@link Inet4Address} 列表 + * @return IP地址字符串列表 + * @since 4.5.17 + */ + public static LinkedHashSet toIpList(Set addressList) { + final LinkedHashSet ipSet = new LinkedHashSet<>(); + for (InetAddress address : addressList) { + ipSet.add(address.getHostAddress()); + } + + return ipSet; + } + + /** + * 获得本机的IP地址列表(包括Ipv4和Ipv6)
+ * 返回的IP列表有序,按照系统设备顺序 + * + * @return IP地址列表 {@link LinkedHashSet} + */ + public static LinkedHashSet localIps() { + final LinkedHashSet localAddressList = localAddressList(null); + return toIpList(localAddressList); + } + + /** + * 获取所有满足过滤条件的本地IP地址对象 + * + * @param addressFilter 过滤器,null表示不过滤,获取所有地址 + * @return 过滤后的地址对象列表 + * @since 4.5.17 + */ + public static LinkedHashSet localAddressList(Filter addressFilter) { + return localAddressList(null, addressFilter); + } + + /** + * 获取所有满足过滤条件的本地IP地址对象 + * + * @param addressFilter 过滤器,null表示不过滤,获取所有地址 + * @param networkInterfaceFilter 过滤器,null表示不过滤,获取所有网卡 + * @return 过滤后的地址对象列表 + */ + public static LinkedHashSet localAddressList(Filter networkInterfaceFilter, Filter addressFilter) { + Enumeration networkInterfaces; + try { + networkInterfaces = NetworkInterface.getNetworkInterfaces(); + } catch (SocketException e) { + throw new UtilException(e); + } + + if (networkInterfaces == null) { + throw new UtilException("Get network interface error!"); + } + + final LinkedHashSet ipSet = new LinkedHashSet<>(); + + while (networkInterfaces.hasMoreElements()) { + final NetworkInterface networkInterface = networkInterfaces.nextElement(); + if (networkInterfaceFilter != null && !networkInterfaceFilter.accept(networkInterface)) { + continue; + } + final Enumeration inetAddresses = networkInterface.getInetAddresses(); + while (inetAddresses.hasMoreElements()) { + final InetAddress inetAddress = inetAddresses.nextElement(); + if (inetAddress != null && (null == addressFilter || addressFilter.accept(inetAddress))) { + ipSet.add(inetAddress); + } + } + } + + return ipSet; + } + + /** + * 获取本机网卡IP地址,这个地址为所有网卡中非回路地址的第一个
+ * 如果获取失败调用 {@link InetAddress#getLocalHost()}方法获取。
+ * 此方法不会抛出异常,获取失败将返回{@code null}
+ *

+ * 参考:http://stackoverflow.com/questions/9481865/getting-the-ip-address-of-the-current-machine-using-java + * + * @return 本机网卡IP地址,获取失败返回{@code null} + * @since 3.0.7 + */ + public static String getLocalhostStr() { + InetAddress localhost = getLocalhost(); + if (null != localhost) { + return localhost.getHostAddress(); + } + return null; + } + + /** + * 获取本机网卡IP地址,规则如下: + * + *

+	 * 1. 查找所有网卡地址,必须非回路(loopback)地址、非局域网地址(siteLocal)、IPv4地址
+	 * 2. 如果无满足要求的地址,调用 {@link InetAddress#getLocalHost()} 获取地址
+	 * 
+ *

+ * 此方法不会抛出异常,获取失败将返回{@code null}
+ *

+ * 见:https://github.com/dromara/hutool/issues/428 + * + * @return 本机网卡IP地址,获取失败返回{@code null} + * @since 3.0.1 + */ + public static InetAddress getLocalhost() { + final LinkedHashSet localAddressList = localAddressList(address -> { + // 非loopback地址,指127.*.*.*的地址 + return !address.isLoopbackAddress() + // 需为IPV4地址 + && address instanceof Inet4Address; + }); + + if (CollUtil.isNotEmpty(localAddressList)) { + InetAddress address2 = null; + for (InetAddress inetAddress : localAddressList) { + if (!inetAddress.isSiteLocalAddress()) { + // 非地区本地地址,指10.0.0.0 ~ 10.255.255.255、172.16.0.0 ~ 172.31.255.255、192.168.0.0 ~ 192.168.255.255 + return inetAddress; + } else if (null == address2) { + address2 = inetAddress; + } + } + + if (null != address2) { + return address2; + } + } + + try { + return InetAddress.getLocalHost(); + } catch (UnknownHostException e) { + // ignore + } + + return null; + } + + /** + * 获得本机MAC地址 + * + * @return 本机MAC地址 + */ + public static String getLocalMacAddress() { + return getMacAddress(getLocalhost()); + } + + /** + * 获得指定地址信息中的MAC地址,使用分隔符“-” + * + * @param inetAddress {@link InetAddress} + * @return MAC地址,用-分隔 + */ + public static String getMacAddress(InetAddress inetAddress) { + return getMacAddress(inetAddress, "-"); + } + + /** + * 获得指定地址信息中的MAC地址 + * + * @param inetAddress {@link InetAddress} + * @param separator 分隔符,推荐使用“-”或者“:” + * @return MAC地址,用-分隔 + */ + public static String getMacAddress(InetAddress inetAddress, String separator) { + if (null == inetAddress) { + return null; + } + + final byte[] mac = getHardwareAddress(inetAddress); + if (null != mac) { + final StringBuilder sb = new StringBuilder(); + String s; + for (int i = 0; i < mac.length; i++) { + if (i != 0) { + sb.append(separator); + } + // 字节转换为整数 + s = Integer.toHexString(mac[i] & 0xFF); + sb.append(s.length() == 1 ? 0 + s : s); + } + return sb.toString(); + } + + return null; + } + + /** + * 获得指定地址信息中的硬件地址 + * + * @param inetAddress {@link InetAddress} + * @return 硬件地址 + * @since 5.7.3 + */ + public static byte[] getHardwareAddress(InetAddress inetAddress) { + if (null == inetAddress) { + return null; + } + + try { + final NetworkInterface networkInterface = NetworkInterface.getByInetAddress(inetAddress); + if (null != networkInterface) { + return networkInterface.getHardwareAddress(); + } + } catch (SocketException e) { + throw new UtilException(e); + } + return null; + } + + /** + * 获得本机物理地址 + * + * @return 本机物理地址 + * @since 5.7.3 + */ + public static byte[] getLocalHardwareAddress() { + return getHardwareAddress(getLocalhost()); + } + + /** + * 获取主机名称,一次获取会缓存名称 + * + * @return 主机名称 + * @since 5.4.4 + */ + public static String getLocalHostName() { + if (StrUtil.isNotBlank(localhostName)) { + return localhostName; + } + + final InetAddress localhost = getLocalhost(); + if (null != localhost) { + String name = localhost.getHostName(); + if (StrUtil.isEmpty(name)) { + name = localhost.getHostAddress(); + } + localhostName = name; + } + + return localhostName; + } + + /** + * 创建 {@link InetSocketAddress} + * + * @param host 域名或IP地址,空表示任意地址 + * @param port 端口,0表示系统分配临时端口 + * @return {@link InetSocketAddress} + * @since 3.3.0 + */ + public static InetSocketAddress createAddress(String host, int port) { + if (StrUtil.isBlank(host)) { + return new InetSocketAddress(port); + } + return new InetSocketAddress(host, port); + } + + /** + * 简易的使用Socket发送数据 + * + * @param host Server主机 + * @param port Server端口 + * @param isBlock 是否阻塞方式 + * @param data 需要发送的数据 + * @throws IORuntimeException IO异常 + * @since 3.3.0 + */ + public static void netCat(String host, int port, boolean isBlock, ByteBuffer data) throws IORuntimeException { + try (SocketChannel channel = SocketChannel.open(createAddress(host, port))) { + channel.configureBlocking(isBlock); + channel.write(data); + } catch (IOException e) { + throw new IORuntimeException(e); + } + } + + /** + * 使用普通Socket发送数据 + * + * @param host Server主机 + * @param port Server端口 + * @param data 数据 + * @throws IORuntimeException IO异常 + * @since 3.3.0 + */ + public static void netCat(String host, int port, byte[] data) throws IORuntimeException { + OutputStream out = null; + try (Socket socket = new Socket(host, port)) { + out = socket.getOutputStream(); + out.write(data); + out.flush(); + } catch (IOException e) { + throw new IORuntimeException(e); + } finally { + IoUtil.close(out); + } + } + + /** + * 是否在CIDR规则配置范围内
+ * 方法来自:【成都】小邓 + * + * @param ip 需要验证的IP + * @param cidr CIDR规则 + * @return 是否在范围内 + * @since 4.0.6 + */ + public static boolean isInRange(String ip, String cidr) { + final int maskSplitMarkIndex = cidr.lastIndexOf(Ipv4Util.IP_MASK_SPLIT_MARK); + if (maskSplitMarkIndex < 0) { + throw new IllegalArgumentException("Invalid cidr: " + cidr); + } + + final long mask = (-1L << 32 - Integer.parseInt(cidr.substring(maskSplitMarkIndex + 1))); + long cidrIpAddr = ipv4ToLong(cidr.substring(0, maskSplitMarkIndex)); + + return (ipv4ToLong(ip) & mask) == (cidrIpAddr & mask); + } + + /** + * Unicode域名转puny code + * + * @param unicode Unicode域名 + * @return puny code + * @since 4.1.22 + */ + public static String idnToASCII(String unicode) { + return IDN.toASCII(unicode); + } + + /** + * 从多级反向代理中获得第一个非unknown IP地址 + * + * @param ip 获得的IP地址 + * @return 第一个非unknown IP地址 + * @since 4.4.1 + */ + public static String getMultistageReverseProxyIp(String ip) { + // 多级反向代理检测 + if (ip != null && StrUtil.indexOf(ip, ',') > 0) { + final List ips = StrUtil.splitTrim(ip, ','); + for (final String subIp : ips) { + if (!isUnknown(subIp)) { + ip = subIp; + break; + } + } + } + return ip; + } + + /** + * 检测给定字符串是否为未知,多用于检测HTTP请求相关
+ * + * @param checkString 被检测的字符串 + * @return 是否未知 + * @since 5.2.6 + */ + public static boolean isUnknown(String checkString) { + return StrUtil.isBlank(checkString) || "unknown".equalsIgnoreCase(checkString); + } + + /** + * 检测IP地址是否能ping通 + * + * @param ip IP地址 + * @return 返回是否ping通 + */ + public static boolean ping(String ip) { + return ping(ip, 200); + } + + /** + * 检测IP地址是否能ping通 + * + * @param ip IP地址 + * @param timeout 检测超时(毫秒) + * @return 是否ping通 + */ + public static boolean ping(String ip, int timeout) { + try { + return InetAddress.getByName(ip).isReachable(timeout); // 当返回值是true时,说明host是可用的,false则不可。 + } catch (Exception ex) { + return false; + } + } + + /** + * 解析Cookie信息 + * + * @param cookieStr Cookie字符串 + * @return cookie字符串 + * @since 5.2.6 + */ + public static List parseCookies(String cookieStr) { + if (StrUtil.isBlank(cookieStr)) { + return Collections.emptyList(); + } + return HttpCookie.parse(cookieStr); + } + + /** + * 检查远程端口是否开启 + * + * @param address 远程地址 + * @param timeout 检测超时 + * @return 远程端口是否开启 + * @since 5.3.2 + */ + public static boolean isOpen(InetSocketAddress address, int timeout) { + try (Socket sc = new Socket()) { + sc.connect(address, timeout); + return true; + } catch (Exception e) { + return false; + } + } + + /** + * 设置全局验证 + * + * @param user 用户名 + * @param pass 密码,考虑安全,此处不使用String + * @since 5.7.2 + */ + public static void setGlobalAuthenticator(String user, char[] pass) { + setGlobalAuthenticator(new UserPassAuthenticator(user, pass)); + } + + /** + * 设置全局验证 + * + * @param authenticator 验证器 + * @since 5.7.2 + */ + public static void setGlobalAuthenticator(Authenticator authenticator) { + Authenticator.setDefault(authenticator); + } + + + // ----------------------------------------------------------------------------------------- Private method start + + // ----------------------------------------------------------------------------------------- Private method end +} diff --git a/src/main/java/cn/hutool/core/net/PassAuth.java b/src/main/java/cn/hutool/core/net/PassAuth.java new file mode 100644 index 0000000..6cda111 --- /dev/null +++ b/src/main/java/cn/hutool/core/net/PassAuth.java @@ -0,0 +1,41 @@ +package cn.hutool.core.net; + +import java.net.Authenticator; +import java.net.PasswordAuthentication; + +/** + * 账号密码形式的{@link Authenticator} 实现。 + * + * @author looly + * @since 5.5.3 + */ +public class PassAuth extends Authenticator { + + /** + * 创建账号密码形式的{@link Authenticator} 实现。 + * + * @param user 用户名 + * @param pass 密码 + * @return PassAuth + */ + public static PassAuth of(String user, char[] pass) { + return new PassAuth(user, pass); + } + + private final PasswordAuthentication auth; + + /** + * 构造 + * + * @param user 用户名 + * @param pass 密码 + */ + public PassAuth(String user, char[] pass) { + auth = new PasswordAuthentication(user, pass); + } + + @Override + protected PasswordAuthentication getPasswordAuthentication() { + return auth; + } +} diff --git a/src/main/java/cn/hutool/core/net/RFC3986.java b/src/main/java/cn/hutool/core/net/RFC3986.java new file mode 100644 index 0000000..d3811b1 --- /dev/null +++ b/src/main/java/cn/hutool/core/net/RFC3986.java @@ -0,0 +1,104 @@ +package cn.hutool.core.net; + +import cn.hutool.core.codec.PercentCodec; + +/** + * RFC3986 编码实现
+ * 定义见:https://www.ietf.org/rfc/rfc3986.html#appendix-A + * + * @author looly + * @since 5.7.16 + */ +public class RFC3986 { + + /** + * gen-delims = ":" / "/" / "?" / "#" / "[" / "]" / "@" + */ + public static final PercentCodec GEN_DELIMS = PercentCodec.of(":/?#[]@"); + + /** + * sub-delims = "!" / "$" / "{@code &}" / "'" / "(" / ")" / "*" / "+" / "," / ";" / "=" + */ + public static final PercentCodec SUB_DELIMS = PercentCodec.of("!$&'()*+,;="); + + /** + * reserved = gen-delims / sub-delims
+ * see:https://www.ietf.org/rfc/rfc3986.html#section-2.2 + */ + public static final PercentCodec RESERVED = GEN_DELIMS.orNew(SUB_DELIMS); + + /** + * unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
+ * see: https://www.ietf.org/rfc/rfc3986.html#section-2.3 + */ + public static final PercentCodec UNRESERVED = PercentCodec.of(unreservedChars()); + + /** + * pchar = unreserved / pct-encoded / sub-delims / ":" / "@" + */ + public static final PercentCodec PCHAR = UNRESERVED.orNew(SUB_DELIMS).or(PercentCodec.of(":@")); + + /** + * segment = pchar
+ * see: https://www.ietf.org/rfc/rfc3986.html#section-3.3 + */ + public static final PercentCodec SEGMENT = PCHAR; + /** + * segment-nz-nc = SEGMENT ; non-zero-length segment without any colon ":" + */ + public static final PercentCodec SEGMENT_NZ_NC = PercentCodec.of(SEGMENT).removeSafe(':'); + + /** + * path = segment / "/" + */ + public static final PercentCodec PATH = SEGMENT.orNew(PercentCodec.of("/")); + + /** + * query = pchar / "/" / "?" + */ + public static final PercentCodec QUERY = PCHAR.orNew(PercentCodec.of("/?")); + + /** + * fragment = pchar / "/" / "?" + */ + public static final PercentCodec FRAGMENT = QUERY; + + /** + * query中的value
+ * value不能包含"{@code &}",可以包含 "=" + */ + public static final PercentCodec QUERY_PARAM_VALUE = PercentCodec.of(QUERY).removeSafe('&'); + + /** + * query中的key
+ * key不能包含"{@code &}" 和 "=" + */ + public static final PercentCodec QUERY_PARAM_NAME = PercentCodec.of(QUERY_PARAM_VALUE).removeSafe('='); + + /** + * unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" + * + * @return unreserved字符 + */ + private static StringBuilder unreservedChars() { + StringBuilder sb = new StringBuilder(); + + // ALPHA + for (char c = 'A'; c <= 'Z'; c++) { + sb.append(c); + } + for (char c = 'a'; c <= 'z'; c++) { + sb.append(c); + } + + // DIGIT + for (char c = '0'; c <= '9'; c++) { + sb.append(c); + } + + // "-" / "." / "_" / "~" + sb.append("_.-~"); + + return sb; + } +} diff --git a/src/main/java/cn/hutool/core/net/SSLContextBuilder.java b/src/main/java/cn/hutool/core/net/SSLContextBuilder.java new file mode 100644 index 0000000..f7c783d --- /dev/null +++ b/src/main/java/cn/hutool/core/net/SSLContextBuilder.java @@ -0,0 +1,137 @@ +package cn.hutool.core.net; + +import cn.hutool.core.builder.Builder; +import cn.hutool.core.io.IORuntimeException; +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.StrUtil; + +import javax.net.ssl.KeyManager; +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManager; +import java.security.GeneralSecurityException; +import java.security.KeyManagementException; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; + +/** + * {@link SSLContext}构建器,可以自定义:
+ *

    + *
  • 协议(protocol),默认TLS
  • + *
  • {@link KeyManager},默认空
  • + *
  • {@link TrustManager},默认{@link DefaultTrustManager},即信任全部
  • + *
  • {@link SecureRandom}
  • + *
+ *

+ * 构建后可获得{@link SSLContext},通过调用{@link SSLContext#getSocketFactory()}获取{@link javax.net.ssl.SSLSocketFactory} + * + * @author Looly + * @since 5.5.2 + */ +public class SSLContextBuilder implements SSLProtocols, Builder { + private static final long serialVersionUID = 1L; + + private String protocol = TLS; + private KeyManager[] keyManagers; + private TrustManager[] trustManagers = {DefaultTrustManager.INSTANCE}; + private SecureRandom secureRandom = new SecureRandom(); + + + /** + * 创建 SSLContextBuilder + * + * @return SSLContextBuilder + */ + public static SSLContextBuilder create() { + return new SSLContextBuilder(); + } + + /** + * 设置协议。例如TLS等 + * + * @param protocol 协议 + * @return 自身 + */ + public SSLContextBuilder setProtocol(String protocol) { + if (StrUtil.isNotBlank(protocol)) { + this.protocol = protocol; + } + return this; + } + + /** + * 设置信任信息 + * + * @param trustManagers TrustManager列表 + * @return 自身 + */ + public SSLContextBuilder setTrustManagers(TrustManager... trustManagers) { + if (ArrayUtil.isNotEmpty(trustManagers)) { + this.trustManagers = trustManagers; + } + return this; + } + + /** + * 设置 JSSE key managers + * + * @param keyManagers JSSE key managers + * @return 自身 + */ + public SSLContextBuilder setKeyManagers(KeyManager... keyManagers) { + if (ArrayUtil.isNotEmpty(keyManagers)) { + this.keyManagers = keyManagers; + } + return this; + } + + /** + * 设置 SecureRandom + * + * @param secureRandom SecureRandom + * @return 自己 + */ + public SSLContextBuilder setSecureRandom(SecureRandom secureRandom) { + if (null != secureRandom) { + this.secureRandom = secureRandom; + } + return this; + } + + /** + * 构建{@link SSLContext} + * + * @return {@link SSLContext} + */ + @Override + public SSLContext build() { + return buildQuietly(); + } + + /** + * 构建{@link SSLContext}需要处理异常 + * + * @return {@link SSLContext} + * @throws NoSuchAlgorithmException 无此算法异常 + * @throws KeyManagementException 密钥管理异常 + * @since 5.7.22 + */ + public SSLContext buildChecked() throws NoSuchAlgorithmException, KeyManagementException { + SSLContext sslContext = SSLContext.getInstance(protocol); + sslContext.init(this.keyManagers, this.trustManagers, this.secureRandom); + return sslContext; + } + + /** + * 构建{@link SSLContext} + * + * @return {@link SSLContext} + * @throws IORuntimeException 包装 GeneralSecurityException异常 + */ + public SSLContext buildQuietly() throws IORuntimeException { + try { + return buildChecked(); + } catch (GeneralSecurityException e) { + throw new IORuntimeException(e); + } + } +} diff --git a/src/main/java/cn/hutool/core/net/SSLProtocols.java b/src/main/java/cn/hutool/core/net/SSLProtocols.java new file mode 100644 index 0000000..32e938f --- /dev/null +++ b/src/main/java/cn/hutool/core/net/SSLProtocols.java @@ -0,0 +1,40 @@ +package cn.hutool.core.net; + +/** + * SSL或TLS协议 + * + * @author looly + * @since 5.7.8 + */ +public interface SSLProtocols { + + /** + * Supports some version of SSL; may support other versions + */ + String SSL = "SSL"; + /** + * Supports SSL version 2 or later; may support other versions + */ + String SSLv2 = "SSLv2"; + /** + * Supports SSL version 3; may support other versions + */ + String SSLv3 = "SSLv3"; + + /** + * Supports some version of TLS; may support other versions + */ + String TLS = "TLS"; + /** + * Supports RFC 2246: TLS version 1.0 ; may support other versions + */ + String TLSv1 = "TLSv1"; + /** + * Supports RFC 4346: TLS version 1.1 ; may support other versions + */ + String TLSv11 = "TLSv1.1"; + /** + * Supports RFC 5246: TLS version 1.2 ; may support other versions + */ + String TLSv12 = "TLSv1.2"; +} diff --git a/src/main/java/cn/hutool/core/net/SSLUtil.java b/src/main/java/cn/hutool/core/net/SSLUtil.java new file mode 100644 index 0000000..d608467 --- /dev/null +++ b/src/main/java/cn/hutool/core/net/SSLUtil.java @@ -0,0 +1,60 @@ +package cn.hutool.core.net; + +import cn.hutool.core.io.IORuntimeException; + +import javax.net.ssl.KeyManager; +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManager; + +/** + * SSL(Secure Sockets Layer 安全套接字协议)相关工具封装 + * + * @author looly + * @since 5.5.2 + */ +public class SSLUtil { + + /** + * 创建{@link SSLContext},默认新人全部 + * + * @param protocol SSL协议,例如TLS等 + * @return {@link SSLContext} + * @throws IORuntimeException 包装 GeneralSecurityException异常 + * @since 5.7.8 + */ + public static SSLContext createSSLContext(String protocol) throws IORuntimeException{ + return SSLContextBuilder.create().setProtocol(protocol).build(); + } + + /** + * 创建{@link SSLContext} + * + * @param protocol SSL协议,例如TLS等 + * @param keyManager 密钥管理器,{@code null}表示无 + * @param trustManager 信任管理器, {@code null}表示无 + * @return {@link SSLContext} + * @throws IORuntimeException 包装 GeneralSecurityException异常 + */ + public static SSLContext createSSLContext(String protocol, KeyManager keyManager, TrustManager trustManager) + throws IORuntimeException { + return createSSLContext(protocol, + keyManager == null ? null : new KeyManager[]{keyManager}, + trustManager == null ? null : new TrustManager[]{trustManager}); + } + + /** + * 创建和初始化{@link SSLContext} + * + * @param protocol SSL协议,例如TLS等 + * @param keyManagers 密钥管理器,{@code null}表示无 + * @param trustManagers 信任管理器, {@code null}表示无 + * @return {@link SSLContext} + * @throws IORuntimeException 包装 GeneralSecurityException异常 + */ + public static SSLContext createSSLContext(String protocol, KeyManager[] keyManagers, TrustManager[] trustManagers) throws IORuntimeException { + return SSLContextBuilder.create() + .setProtocol(protocol) + .setKeyManagers(keyManagers) + .setTrustManagers(trustManagers).build(); + } +} diff --git a/src/main/java/cn/hutool/core/net/URLDecoder.java b/src/main/java/cn/hutool/core/net/URLDecoder.java new file mode 100644 index 0000000..106d973 --- /dev/null +++ b/src/main/java/cn/hutool/core/net/URLDecoder.java @@ -0,0 +1,138 @@ +package cn.hutool.core.net; + +import cn.hutool.core.util.CharUtil; +import cn.hutool.core.util.StrUtil; + +import java.io.ByteArrayOutputStream; +import java.io.Serializable; +import java.nio.charset.Charset; + +/** + * URL解码,数据内容的类型是 application/x-www-form-urlencoded。 + * + *

+ * 1. 将%20转换为空格 ;
+ * 2. 将"%xy"转换为文本形式,xy是两位16进制的数值;
+ * 3. 跳过不符合规范的%形式,直接输出
+ * 
+ * + * @author looly + */ +public class URLDecoder implements Serializable { + private static final long serialVersionUID = 1L; + + private static final byte ESCAPE_CHAR = '%'; + + /** + * 解码,不对+解码 + * + *
    + *
  1. 将%20转换为空格
  2. + *
  3. 将 "%xy"转换为文本形式,xy是两位16进制的数值
  4. + *
  5. 跳过不符合规范的%形式,直接输出
  6. + *
+ * + * @param str 包含URL编码后的字符串 + * @param charset 编码 + * @return 解码后的字符串 + */ + public static String decodeForPath(String str, Charset charset) { + return decode(str, charset, false); + } + + /** + * 解码
+ * 规则见:https://url.spec.whatwg.org/#urlencoded-parsing + *
+	 *   1. 将+和%20转换为空格(" ");
+	 *   2. 将"%xy"转换为文本形式,xy是两位16进制的数值;
+	 *   3. 跳过不符合规范的%形式,直接输出
+	 * 
+ * + * @param str 包含URL编码后的字符串 + * @param charset 编码 + * @return 解码后的字符串 + */ + public static String decode(String str, Charset charset) { + return decode(str, charset, true); + } + + /** + * 解码 + *
+	 *   1. 将%20转换为空格 ;
+	 *   2. 将"%xy"转换为文本形式,xy是两位16进制的数值;
+	 *   3. 跳过不符合规范的%形式,直接输出
+	 * 
+ * + * @param str 包含URL编码后的字符串 + * @param isPlusToSpace 是否+转换为空格 + * @param charset 编码,{@code null}表示不做编码 + * @return 解码后的字符串 + */ + public static String decode(String str, Charset charset, boolean isPlusToSpace) { + if(null == charset){ + return str; + } + return StrUtil.str(decode(StrUtil.bytes(str, charset), isPlusToSpace), charset); + } + + /** + * 解码 + *
+	 *   1. 将+和%20转换为空格 ;
+	 *   2. 将"%xy"转换为文本形式,xy是两位16进制的数值;
+	 *   3. 跳过不符合规范的%形式,直接输出
+	 * 
+ * + * @param bytes url编码的bytes + * @return 解码后的bytes + */ + public static byte[] decode(byte[] bytes) { + return decode(bytes, true); + } + + /** + * 解码 + *
+	 *   1. 将%20转换为空格 ;
+	 *   2. 将"%xy"转换为文本形式,xy是两位16进制的数值;
+	 *   3. 跳过不符合规范的%形式,直接输出
+	 * 
+ * + * @param bytes url编码的bytes + * @param isPlusToSpace 是否+转换为空格 + * @return 解码后的bytes + * @since 5.6.3 + */ + public static byte[] decode(byte[] bytes, boolean isPlusToSpace) { + if (bytes == null) { + return null; + } + final ByteArrayOutputStream buffer = new ByteArrayOutputStream(bytes.length); + int b; + for (int i = 0; i < bytes.length; i++) { + b = bytes[i]; + if (b == '+') { + buffer.write(isPlusToSpace ? CharUtil.SPACE : b); + } else if (b == ESCAPE_CHAR) { + if (i + 1 < bytes.length) { + final int u = CharUtil.digit16(bytes[i + 1]); + if (u >= 0 && i + 2 < bytes.length) { + final int l = CharUtil.digit16(bytes[i + 2]); + if (l >= 0) { + buffer.write((char) ((u << 4) + l)); + i += 2; + continue; + } + } + } + // 跳过不符合规范的%形式 + buffer.write(b); + } else { + buffer.write(b); + } + } + return buffer.toByteArray(); + } +} diff --git a/src/main/java/cn/hutool/core/net/URLEncodeUtil.java b/src/main/java/cn/hutool/core/net/URLEncodeUtil.java new file mode 100644 index 0000000..c6fd983 --- /dev/null +++ b/src/main/java/cn/hutool/core/net/URLEncodeUtil.java @@ -0,0 +1,187 @@ +package cn.hutool.core.net; + +import cn.hutool.core.exceptions.UtilException; +import cn.hutool.core.util.CharsetUtil; +import cn.hutool.core.util.StrUtil; + +import java.nio.charset.Charset; + +/** + * URL编码工具
+ * TODO 在6.x中移除此工具(无法很好区分URL编码和www-form编码) + * + * @since 5.7.13 + * @author looly + */ +public class URLEncodeUtil { + /** + * 编码URL,默认使用UTF-8编码
+ * 将需要转换的内容(ASCII码形式之外的内容),用十六进制表示法转换出来,并在之前加上%开头。 + * + * @param url URL + * @return 编码后的URL + * @throws UtilException UnsupportedEncodingException + */ + public static String encodeAll(String url) { + return encodeAll(url, CharsetUtil.CHARSET_UTF_8); + } + + /** + * 编码URL
+ * 将需要转换的内容(ASCII码形式之外的内容),用十六进制表示法转换出来,并在之前加上%开头。 + * + * @param url URL + * @param charset 编码,为null表示不编码 + * @return 编码后的URL + * @throws UtilException UnsupportedEncodingException + */ + public static String encodeAll(String url, Charset charset) throws UtilException { + return RFC3986.UNRESERVED.encode(url, charset); + } + + /** + * 编码URL,默认使用UTF-8编码
+ * 将需要转换的内容(ASCII码形式之外的内容),用十六进制表示法转换出来,并在之前加上%开头。
+ * 此方法用于URL自动编码,类似于浏览器中键入地址自动编码,对于像类似于“/”的字符不再编码 + * + * @param url URL + * @return 编码后的URL + * @throws UtilException UnsupportedEncodingException + * @since 3.1.2 + */ + public static String encode(String url) throws UtilException { + return encode(url, CharsetUtil.CHARSET_UTF_8); + } + + /** + * 编码字符为 application/x-www-form-urlencoded
+ * 将需要转换的内容(ASCII码形式之外的内容),用十六进制表示法转换出来,并在之前加上%开头。
+ * 此方法用于URL自动编码,类似于浏览器中键入地址自动编码,对于像类似于“/”的字符不再编码 + * + * @param url 被编码内容 + * @param charset 编码 + * @return 编码后的字符 + * @since 4.4.1 + */ + public static String encode(String url, Charset charset) { + return RFC3986.PATH.encode(url, charset); + } + + /** + * 编码URL,默认使用UTF-8编码
+ * 将需要转换的内容(ASCII码形式之外的内容),用十六进制表示法转换出来,并在之前加上%开头。
+ * 此方法用于POST请求中的请求体自动编码,转义大部分特殊字符 + * + * @param url URL + * @return 编码后的URL + * @throws UtilException UnsupportedEncodingException + * @since 3.1.2 + */ + public static String encodeQuery(String url) throws UtilException { + return encodeQuery(url, CharsetUtil.CHARSET_UTF_8); + } + + /** + * 编码字符为URL中查询语句
+ * 将需要转换的内容(ASCII码形式之外的内容),用十六进制表示法转换出来,并在之前加上%开头。
+ * 此方法用于POST请求中的请求体自动编码,转义大部分特殊字符 + * + * @param url 被编码内容 + * @param charset 编码 + * @return 编码后的字符 + * @since 4.4.1 + */ + public static String encodeQuery(String url, Charset charset) { + return RFC3986.QUERY.encode(url, charset); + } + + /** + * 编码URL,默认使用UTF-8编码
+ * 将需要转换的内容(ASCII码形式之外的内容),用十六进制表示法转换出来,并在之前加上%开头。
+ * 此方法用于URL的Segment中自动编码,转义大部分特殊字符 + * + *
+	 * pchar = unreserved(不处理) / pct-encoded / sub-delims(子分隔符) / "@"
+	 * unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
+	 * sub-delims = "!" / "$" / "&" / "'" / "(" / ")" / "*" / "+" / "," / ";" / "="
+	 * 
+ * + * @param url URL + * @return 编码后的URL + * @throws UtilException UnsupportedEncodingException + * @since 5.6.5 + */ + public static String encodePathSegment(String url) throws UtilException { + return encodePathSegment(url, CharsetUtil.CHARSET_UTF_8); + } + + /** + * 编码字符为URL中查询语句
+ * 将需要转换的内容(ASCII码形式之外的内容),用十六进制表示法转换出来,并在之前加上%开头。
+ * 此方法用于URL的Segment中自动编码,转义大部分特殊字符 + * + *
+	 * pchar = unreserved / pct-encoded / sub-delims / ":" / "@"
+	 * unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
+	 * sub-delims = "!" / "$" / "&" / "'" / "(" / ")" / "*" / "+" / "," / ";" / "="
+	 * 
+ * + * @param url 被编码内容 + * @param charset 编码 + * @return 编码后的字符 + * @since 5.6.5 + */ + public static String encodePathSegment(String url, Charset charset) { + if (StrUtil.isEmpty(url)) { + return url; + } + return RFC3986.SEGMENT.encode(url, charset); + } + + /** + * 编码URL,默认使用UTF-8编码
+ * URL的Fragment URLEncoder
+ * 默认的编码器针对Fragment,定义如下: + * + *
+	 * fragment    = *( pchar / "/" / "?" )
+	 * pchar       = unreserved / pct-encoded / sub-delims / ":" / "@"
+	 * unreserved  = ALPHA / DIGIT / "-" / "." / "_" / "~"
+	 * sub-delims  = "!" / "$" / "&" / "'" / "(" / ")" / "*" / "+" / "," / ";" / "="
+	 * 
+ * + * 具体见:https://datatracker.ietf.org/doc/html/rfc3986#section-3.5 + * + * @param url 被编码内容 + * @return 编码后的字符 + * @since 5.7.13 + */ + public static String encodeFragment(String url) throws UtilException { + return encodeFragment(url, CharsetUtil.CHARSET_UTF_8); + } + + /** + * URL的Fragment URLEncoder
+ * 默认的编码器针对Fragment,定义如下: + * + *
+	 * fragment    = *( pchar / "/" / "?" )
+	 * pchar       = unreserved / pct-encoded / sub-delims / ":" / "@"
+	 * unreserved  = ALPHA / DIGIT / "-" / "." / "_" / "~"
+	 * sub-delims  = "!" / "$" / "&" / "'" / "(" / ")" / "*" / "+" / "," / ";" / "="
+	 * 
+ * + * 具体见:https://datatracker.ietf.org/doc/html/rfc3986#section-3.5 + * + * @param url 被编码内容 + * @param charset 编码 + * @return 编码后的字符 + * @since 5.7.13 + */ + public static String encodeFragment(String url, Charset charset) { + if (StrUtil.isEmpty(url)) { + return url; + } + return RFC3986.FRAGMENT.encode(url, charset); + } +} diff --git a/src/main/java/cn/hutool/core/net/URLEncoder.java b/src/main/java/cn/hutool/core/net/URLEncoder.java new file mode 100644 index 0000000..ee519ea --- /dev/null +++ b/src/main/java/cn/hutool/core/net/URLEncoder.java @@ -0,0 +1,407 @@ +package cn.hutool.core.net; + +import cn.hutool.core.util.CharUtil; +import cn.hutool.core.util.HexUtil; +import cn.hutool.core.util.StrUtil; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.io.Serializable; +import java.nio.charset.Charset; +import java.util.BitSet; + +/** + * URL编码,数据内容的类型是 application/x-www-form-urlencoded。 + * TODO 6.x移除此类,使用PercentCodec代替(无法很好区分URL编码和www-form编码) + * + *
+ * 1.字符"a"-"z","A"-"Z","0"-"9",".","-","*",和"_" 都不会被编码;
+ * 2.将空格转换为%20 ;
+ * 3.将非文本内容转换成"%xy"的形式,xy是两位16进制的数值;
+ * 
+ * + * @author looly + * @see cn.hutool.core.codec.PercentCodec + * @deprecated 此类中的方法并不规范,请使用 {@link RFC3986} + */ +@Deprecated +public class URLEncoder implements Serializable { + private static final long serialVersionUID = 1L; + + // --------------------------------------------------------------------------------------------- Static method start + /** + * 默认URLEncoder
+ * 默认的编码器针对URI路径编码,定义如下: + * + *
+	 * default = pchar / "/"
+	 * pchar = unreserved(不处理) / pct-encoded / sub-delims(子分隔符) / ":" / "@"
+	 * unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
+	 * sub-delims = "!" / "$" / "&" / "'" / "(" / ")" / "*" / "+" / "," / ";" / "="
+	 * 
+ */ + public static final URLEncoder DEFAULT = createDefault(); + + /** + * URL的Path的每一个Segment URLEncoder
+ * 默认的编码器针对URI路径编码,定义如下: + * + *
+	 * pchar = unreserved / pct-encoded / sub-delims / ":"(非空segment不包含:) / "@"
+	 * unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
+	 * sub-delims = "!" / "$" / "&" / "'" / "(" / ")" / "*" / "+" / "," / ";" / "="
+	 * 
+ * + * 定义见:https://www.rfc-editor.org/rfc/rfc3986.html#section-3.3 + */ + public static final URLEncoder PATH_SEGMENT = createPathSegment(); + + /** + * URL的Fragment URLEncoder
+ * 默认的编码器针对Fragment,定义如下: + * + *
+	 * fragment    = *( pchar / "/" / "?" )
+	 * pchar       = unreserved / pct-encoded / sub-delims / ":" / "@"
+	 * unreserved  = ALPHA / DIGIT / "-" / "." / "_" / "~"
+	 * sub-delims  = "!" / "$" / "&" / "'" / "(" / ")" / "*" / "+" / "," / ";" / "="
+	 * 
+ * + * 具体见:https://datatracker.ietf.org/doc/html/rfc3986#section-3.5 + * @since 5.7.13 + */ + public static final URLEncoder FRAGMENT = createFragment(); + + /** + * 用于查询语句的URLEncoder
+ * 编码器针对URI路径编码,定义如下: + * + *
+	 * 0x20 ' ' =》 '+'
+	 * 0x2A, 0x2D, 0x2E, 0x30 to 0x39, 0x41 to 0x5A, 0x5F, 0x61 to 0x7A as-is
+	 * '*', '-', '.', '0' to '9', 'A' to 'Z', '_', 'a' to 'z' Also '=' and '&' 不编码
+	 * 其它编码为 %nn 形式
+	 * 
+ *

+ * 详细见:https://www.w3.org/TR/html5/forms.html#application/x-www-form-urlencoded-encoding-algorithm + */ + public static final URLEncoder QUERY = createQuery(); + + /** + * 全编码的URLEncoder
+ *

+	 *  0x2A, 0x2D, 0x2E, 0x30 to 0x39, 0x41 to 0x5A, 0x5F, 0x61 to 0x7A as-is
+	 *  '*', '-', '.', '0' to '9', 'A' to 'Z', '_', 'a' to 'z' 不编码
+	 *  其它编码为 %nn 形式
+	 * 
+ */ + public static final URLEncoder ALL = createAll(); + + /** + * 创建默认URLEncoder
+ * 默认的编码器针对URI路径编码,定义如下: + * + *
+	 * default = pchar / "/"
+	 * pchar = unreserved(不处理) / pct-encoded / sub-delims(子分隔符) / ":" / "@"
+	 * unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
+	 * sub-delims = "!" / "$" / "&" / "'" / "(" / ")" / "*" / "+" / "," / ";" / "="
+	 * 
+ * + * @return URLEncoder + */ + public static URLEncoder createDefault() { + final URLEncoder encoder = new URLEncoder(); + encoder.addSafeCharacter('-'); + encoder.addSafeCharacter('.'); + encoder.addSafeCharacter('_'); + encoder.addSafeCharacter('~'); + + // Add the sub-delims + addSubDelims(encoder); + + // Add the remaining literals + encoder.addSafeCharacter(':'); + encoder.addSafeCharacter('@'); + + // Add '/' so it isn't encoded when we encode a path + encoder.addSafeCharacter('/'); + + return encoder; + } + + /** + * URL的Path的每一个Segment URLEncoder
+ * 默认的编码器针对URI路径的每一段编码,定义如下: + * + *
+	 * pchar = unreserved / pct-encoded / sub-delims / ":"(非空segment不包含:) / "@"
+	 * unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
+	 * sub-delims = "!" / "$" / "&" / "'" / "(" / ")" / "*" / "+" / "," / ";" / "="
+	 * 
+ * + * 定义见:https://www.rfc-editor.org/rfc/rfc3986.html#section-3.3 + * + * @return URLEncoder + */ + public static URLEncoder createPathSegment() { + final URLEncoder encoder = new URLEncoder(); + + // unreserved + encoder.addSafeCharacter('-'); + encoder.addSafeCharacter('.'); + encoder.addSafeCharacter('_'); + encoder.addSafeCharacter('~'); + + // Add the sub-delims + addSubDelims(encoder); + + // Add the remaining literals + //non-zero-length segment without any colon ":" + //encoder.addSafeCharacter(':'); + encoder.addSafeCharacter('@'); + + return encoder; + } + + /** + * URL的Fragment URLEncoder
+ * 默认的编码器针对Fragment,定义如下: + * + *
+	 * fragment    = *( pchar / "/" / "?" )
+	 * pchar       = unreserved / pct-encoded / sub-delims / ":" / "@"
+	 * unreserved  = ALPHA / DIGIT / "-" / "." / "_" / "~"
+	 * sub-delims  = "!" / "$" / "&" / "'" / "(" / ")" / "*" / "+" / "," / ";" / "="
+	 * 
+ * + * 具体见:https://datatracker.ietf.org/doc/html/rfc3986#section-3.5 + * + * @return URLEncoder + * @since 5.7.13 + */ + public static URLEncoder createFragment() { + final URLEncoder encoder = new URLEncoder(); + encoder.addSafeCharacter('-'); + encoder.addSafeCharacter('.'); + encoder.addSafeCharacter('_'); + encoder.addSafeCharacter('~'); + + // Add the sub-delims + addSubDelims(encoder); + + // Add the remaining literals + encoder.addSafeCharacter(':'); + encoder.addSafeCharacter('@'); + + encoder.addSafeCharacter('/'); + encoder.addSafeCharacter('?'); + + return encoder; + } + + /** + * 创建用于查询语句的URLEncoder
+ * 编码器针对URI路径编码,定义如下: + * + *
+	 * 0x20 ' ' =》 '+'
+	 * 0x2A, 0x2D, 0x2E, 0x30 to 0x39, 0x41 to 0x5A, 0x5F, 0x61 to 0x7A as-is
+	 * '*', '-', '.', '0' to '9', 'A' to 'Z', '_', 'a' to 'z' Also '=' and '&' 不编码
+	 * 其它编码为 %nn 形式
+	 * 
+ *

+ * 详细见:https://www.w3.org/TR/html5/forms.html#application/x-www-form-urlencoded-encoding-algorithm + * + * @return URLEncoder + */ + public static URLEncoder createQuery() { + final URLEncoder encoder = new URLEncoder(); + // Special encoding for space + encoder.setEncodeSpaceAsPlus(true); + // Alpha and digit are safe by default + // Add the other permitted characters + encoder.addSafeCharacter('*'); + encoder.addSafeCharacter('-'); + encoder.addSafeCharacter('.'); + encoder.addSafeCharacter('_'); + + encoder.addSafeCharacter('='); + encoder.addSafeCharacter('&'); + + return encoder; + } + + /** + * 创建URLEncoder
+ * 编码器针对URI路径编码,定义如下: + * + *

+	 * 0x2A, 0x2D, 0x2E, 0x30 to 0x39, 0x41 to 0x5A, 0x5F, 0x61 to 0x7A as-is
+	 * '*', '-', '.', '0' to '9', 'A' to 'Z', '_', 'a' to 'z' 不编码
+	 * 其它编码为 %nn 形式
+	 * 
+ *

+ * 详细见:https://www.w3.org/TR/html5/forms.html#application/x-www-form-urlencoded-encoding-algorithm + * + * @return URLEncoder + */ + public static URLEncoder createAll() { + final URLEncoder encoder = new URLEncoder(); + encoder.addSafeCharacter('*'); + encoder.addSafeCharacter('-'); + encoder.addSafeCharacter('.'); + encoder.addSafeCharacter('_'); + + return encoder; + } + // --------------------------------------------------------------------------------------------- Static method end + + /** + * 存放安全编码 + */ + private final BitSet safeCharacters; + /** + * 是否编码空格为+ + */ + private boolean encodeSpaceAsPlus = false; + + /** + * 构造
+ * [a-zA-Z0-9]默认不被编码 + */ + public URLEncoder() { + this(new BitSet(256)); + + // unreserved + addAlpha(); + addDigit(); + } + + /** + * 构造 + * + * @param safeCharacters 安全字符,安全字符不被编码 + */ + private URLEncoder(BitSet safeCharacters) { + this.safeCharacters = safeCharacters; + } + + /** + * 增加安全字符
+ * 安全字符不被编码 + * + * @param c 字符 + */ + public void addSafeCharacter(char c) { + safeCharacters.set(c); + } + + /** + * 移除安全字符
+ * 安全字符不被编码 + * + * @param c 字符 + */ + public void removeSafeCharacter(char c) { + safeCharacters.clear(c); + } + + /** + * 是否将空格编码为+ + * + * @param encodeSpaceAsPlus 是否将空格编码为+ + */ + public void setEncodeSpaceAsPlus(boolean encodeSpaceAsPlus) { + this.encodeSpaceAsPlus = encodeSpaceAsPlus; + } + + /** + * 将URL中的字符串编码为%形式 + * + * @param path 需要编码的字符串 + * @param charset 编码, {@code null}返回原字符串,表示不编码 + * @return 编码后的字符串 + */ + public String encode(String path, Charset charset) { + if (null == charset || StrUtil.isEmpty(path)) { + return path; + } + + final StringBuilder rewrittenPath = new StringBuilder(path.length()); + ByteArrayOutputStream buf = new ByteArrayOutputStream(); + OutputStreamWriter writer = new OutputStreamWriter(buf, charset); + + int c; + for (int i = 0; i < path.length(); i++) { + c = path.charAt(i); + if (safeCharacters.get(c)) { + rewrittenPath.append((char) c); + } else if (encodeSpaceAsPlus && c == CharUtil.SPACE) { + // 对于空格单独处理 + rewrittenPath.append('+'); + } else { + // convert to external encoding before hex conversion + try { + writer.write((char) c); + writer.flush(); + } catch (IOException e) { + buf.reset(); + continue; + } + + byte[] ba = buf.toByteArray(); + for (byte toEncode : ba) { + // Converting each byte in the buffer + rewrittenPath.append('%'); + HexUtil.appendHex(rewrittenPath, toEncode, false); + } + buf.reset(); + } + } + return rewrittenPath.toString(); + } + + /** + * 增加安全字符[a-z][A-Z] + */ + private void addAlpha() { + for (char i = 'a'; i <= 'z'; i++) { + addSafeCharacter(i); + } + for (char i = 'A'; i <= 'Z'; i++) { + addSafeCharacter(i); + } + } + + /** + * 增加数字1-9 + */ + private void addDigit() { + for (char i = '0'; i <= '9'; i++) { + addSafeCharacter(i); + } + } + + + /** + * 增加sub-delims
+ * sub-delims = "!" / "$" / "&" / "'" / "(" / ") / "*" / "+" / "," / ";" / "=" + * 定义见:https://datatracker.ietf.org/doc/html/rfc3986#section-2.2 + */ + private static void addSubDelims(URLEncoder encoder){ + // Add the sub-delims + encoder.addSafeCharacter('!'); + encoder.addSafeCharacter('$'); + encoder.addSafeCharacter('&'); + encoder.addSafeCharacter('\''); + encoder.addSafeCharacter('('); + encoder.addSafeCharacter(')'); + encoder.addSafeCharacter('*'); + encoder.addSafeCharacter('+'); + encoder.addSafeCharacter(','); + encoder.addSafeCharacter(';'); + encoder.addSafeCharacter('='); + } +} diff --git a/src/main/java/cn/hutool/core/net/UserPassAuthenticator.java b/src/main/java/cn/hutool/core/net/UserPassAuthenticator.java new file mode 100644 index 0000000..0cbf74c --- /dev/null +++ b/src/main/java/cn/hutool/core/net/UserPassAuthenticator.java @@ -0,0 +1,33 @@ +package cn.hutool.core.net; + +import java.net.Authenticator; +import java.net.PasswordAuthentication; + +/** + * 账号密码形式的{@link Authenticator} + * + * @author looly + * @since 5.7.2 + */ +public class UserPassAuthenticator extends Authenticator { + + private final String user; + private final char[] pass; + + /** + * 构造 + * + * @param user 用户名 + * @param pass 密码 + */ + public UserPassAuthenticator(String user, char[] pass) { + this.user = user; + this.pass = pass; + } + + @Override + protected PasswordAuthentication getPasswordAuthentication() { + return new PasswordAuthentication(this.user, this.pass); + } + +} diff --git a/src/main/java/cn/hutool/core/net/multipart/MultipartFormData.java b/src/main/java/cn/hutool/core/net/multipart/MultipartFormData.java new file mode 100644 index 0000000..28c7906 --- /dev/null +++ b/src/main/java/cn/hutool/core/net/multipart/MultipartFormData.java @@ -0,0 +1,273 @@ +package cn.hutool.core.net.multipart; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.convert.Convert; +import cn.hutool.core.map.multi.ListValueMap; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.Charset; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * HttpRequest解析器
+ * 来自Jodd + * + * @author jodd.org + */ +public class MultipartFormData { + + /** 请求参数 */ + private final ListValueMap requestParameters = new ListValueMap<>(); + /** 请求文件 */ + private final ListValueMap requestFiles = new ListValueMap<>(); + /** 上传选项 */ + private final UploadSetting setting; + + /** 是否解析完毕 */ + private boolean loaded; + + // --------------------------------------------------------------------- Constructor start + /** + * 构造 + */ + public MultipartFormData() { + this(null); + } + + /** + * 构造 + * + * @param uploadSetting 上传设定 + */ + public MultipartFormData(UploadSetting uploadSetting) { + this.setting = uploadSetting == null ? new UploadSetting() : uploadSetting; + } + // --------------------------------------------------------------------- Constructor end + + /** + * 提取上传的文件和表单数据 + * + * @param inputStream HttpRequest流 + * @param charset 编码 + * @throws IOException IO异常 + */ + public void parseRequestStream(InputStream inputStream, Charset charset) throws IOException { + setLoaded(); + + MultipartRequestInputStream input = new MultipartRequestInputStream(inputStream); + input.readBoundary(); + while (true) { + UploadFileHeader header = input.readDataHeader(charset); + if (header == null) { + break; + } + + if (header.isFile) { + // 文件类型的表单项 + String fileName = header.fileName; + if (fileName.length() > 0 && header.contentType.contains("application/x-macbinary")) { + input.skipBytes(128); + } + final UploadFile newFile = new UploadFile(header, setting); + if(newFile.processStream(input)){ + putFile(header.formFieldName, newFile); + } + } else { + // 标准表单项 + putParameter(header.formFieldName, input.readString(charset)); + } + + input.skipBytes(1); + input.mark(1); + + // read byte, but may be end of stream + int nextByte = input.read(); + if (nextByte == -1 || nextByte == '-') { + input.reset(); + break; + } + input.reset(); + } + } + + // ---------------------------------------------------------------- parameters + /** + * 返回单一参数值,如果有多个只返回第一个 + * + * @param paramName 参数名 + * @return null未找到,否则返回值 + */ + public String getParam(String paramName) { + final List values = getListParam(paramName); + if (CollUtil.isNotEmpty(values)) { + return values.get(0); + } + return null; + } + + /** + * @return 获得参数名集合 + */ + public Set getParamNames() { + return requestParameters.keySet(); + } + + /** + * 获得数组表单值 + * + * @param paramName 参数名 + * @return 数组表单值 + */ + public String[] getArrayParam(String paramName) { + final List listParam = getListParam(paramName); + if(null != listParam){ + return listParam.toArray(new String[0]); + } + return null; + } + + /** + * 获得集合表单值 + * + * @param paramName 参数名 + * @return 数组表单值 + * @since 5.3.0 + */ + public List getListParam(String paramName) { + return requestParameters.get(paramName); + } + + /** + * 获取所有属性的集合 + * + * @return 所有属性的集合 + */ + public Map getParamMap() { + return Convert.toMap(String.class, String[].class, getParamListMap()); + } + + /** + * 获取所有属性的集合 + * + * @return 所有属性的集合 + */ + public ListValueMap getParamListMap() { + return this.requestParameters; + } + + // --------------------------------------------------------------------------- Files parameters + /** + * 获取上传的文件 + * + * @param paramName 文件参数名称 + * @return 上传的文件, 如果无为null + */ + public UploadFile getFile(String paramName) { + UploadFile[] values = getFiles(paramName); + if ((values != null) && (values.length > 0)) { + return values[0]; + } + return null; + } + + /** + * 获得某个属性名的所有文件
+ * 当表单中两个文件使用同一个name的时候 + * + * @param paramName 属性名 + * @return 上传的文件列表 + */ + public UploadFile[] getFiles(String paramName) { + final List fileList = getFileList(paramName); + if(null != fileList){ + return fileList.toArray(new UploadFile[0]); + } + return null; + } + + /** + * 获得某个属性名的所有文件
+ * 当表单中两个文件使用同一个name的时候 + * + * @param paramName 属性名 + * @return 上传的文件列表 + * @since 5.3.0 + */ + public List getFileList(String paramName) { + return requestFiles.get(paramName); + } + + /** + * 获取上传的文件属性名集合 + * + * @return 上传的文件属性名集合 + */ + public Set getFileParamNames() { + return requestFiles.keySet(); + } + + /** + * 获取文件映射 + * + * @return 文件映射 + */ + public Map getFileMap() { + return Convert.toMap(String.class, UploadFile[].class, getFileListValueMap()); + } + + /** + * 获取文件映射 + * + * @return 文件映射 + */ + public ListValueMap getFileListValueMap() { + return this.requestFiles; + } + + // --------------------------------------------------------------------------- Load + /** + * 是否已被解析 + * + * @return 如果流已被解析返回true + */ + public boolean isLoaded() { + return loaded; + } + + // ---------------------------------------------------------------- Private method start + /** + * 加入上传文件 + * + * @param name 参数名 + * @param uploadFile 文件 + */ + private void putFile(String name, UploadFile uploadFile) { + this.requestFiles.putValue(name, uploadFile); + } + + /** + * 加入普通参数 + * + * @param name 参数名 + * @param value 参数值 + */ + private void putParameter(String name, String value) { + this.requestParameters.putValue(name, value); + } + + /** + * 设置使输入流为解析状态,如果已解析,则抛出异常 + * + * @throws IOException IO异常 + */ + private void setLoaded() throws IOException { + if (loaded) { + throw new IOException("Multi-part request already parsed."); + } + loaded = true; + } + // ---------------------------------------------------------------- Private method end +} diff --git a/src/main/java/cn/hutool/core/net/multipart/MultipartRequestInputStream.java b/src/main/java/cn/hutool/core/net/multipart/MultipartRequestInputStream.java new file mode 100644 index 0000000..643698e --- /dev/null +++ b/src/main/java/cn/hutool/core/net/multipart/MultipartRequestInputStream.java @@ -0,0 +1,242 @@ +package cn.hutool.core.net.multipart; + +import cn.hutool.core.io.FastByteArrayOutputStream; + +import java.io.BufferedInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.charset.Charset; + +/** + * Http请求解析流,提供了专门针对带文件的form表单的解析
+ * 来自Jodd + * + * @author jodd.org + */ +public class MultipartRequestInputStream extends BufferedInputStream { + + public MultipartRequestInputStream(InputStream in) { + super(in); + } + + /** + * 读取byte字节流,在末尾抛出异常 + * + * @return byte + * @throws IOException 读取异常 + */ + public byte readByte() throws IOException { + int i = super.read(); + if (i == -1) { + throw new IOException("End of HTTP request stream reached"); + } + return (byte) i; + } + + /** + * 跳过指定位数的 bytes. + * + * @param i 跳过的byte数 + * @throws IOException IO异常 + */ + public void skipBytes(long i) throws IOException { + long len = super.skip(i); + if (len != i) { + throw new IOException("Unable to skip data in HTTP request"); + } + } + + // ---------------------------------------------------------------- boundary + + /** + * part部分边界 + */ + protected byte[] boundary; + + /** + * 输入流中读取边界 + * + * @return 边界 + * @throws IOException 读取异常 + */ + public byte[] readBoundary() throws IOException { + ByteArrayOutputStream boundaryOutput = new ByteArrayOutputStream(1024); + byte b; + // skip optional whitespaces + //noinspection StatementWithEmptyBody + while ((b = readByte()) <= ' ') { + } + boundaryOutput.write(b); + + // now read boundary chars + while ((b = readByte()) != '\r') { + boundaryOutput.write(b); + } + if (boundaryOutput.size() == 0) { + throw new IOException("Problems with parsing request: invalid boundary"); + } + skipBytes(1); + boundary = new byte[boundaryOutput.size() + 2]; + System.arraycopy(boundaryOutput.toByteArray(), 0, boundary, 2, boundary.length - 2); + boundary[0] = '\r'; + boundary[1] = '\n'; + return boundary; + } + + // ---------------------------------------------------------------- data header + + protected UploadFileHeader lastHeader; + + public UploadFileHeader getLastHeader() { + return lastHeader; + } + + /** + * 从流中读取文件头部信息, 如果达到末尾则返回null + * + * @param encoding 字符集 + * @return 头部信息, 如果达到末尾则返回null + * @throws IOException 读取异常 + */ + public UploadFileHeader readDataHeader(Charset encoding) throws IOException { + String dataHeader = readDataHeaderString(encoding); + if (dataHeader != null) { + lastHeader = new UploadFileHeader(dataHeader); + } else { + lastHeader = null; + } + return lastHeader; + } + + /** + * 读取数据头信息字符串 + * + * @param charset 编码 + * @return 数据头信息字符串 + * @throws IOException IO异常 + */ + protected String readDataHeaderString(Charset charset) throws IOException { + ByteArrayOutputStream data = new ByteArrayOutputStream(); + byte b; + while (true) { + // end marker byte on offset +0 and +2 must be 13 + if ((b = readByte()) != '\r') { + data.write(b); + continue; + } + mark(4); + skipBytes(1); + int i = read(); + if (i == -1) { + // reached end of stream + return null; + } + if (i == '\r') { + reset(); + break; + } + reset(); + data.write(b); + } + skipBytes(3); + return charset == null ? data.toString() : data.toString(charset); + } + // ---------------------------------------------------------------- copy + + /** + * 读取字节流,直到下一个boundary + * + * @param charset 编码,null表示系统默认编码 + * @return 读取的字符串 + * @throws IOException 读取异常 + */ + public String readString(Charset charset) throws IOException { + final FastByteArrayOutputStream out = new FastByteArrayOutputStream(); + copy(out); + return out.toString(charset); + } + + /** + * 字节流复制到out,直到下一个boundary + * + * @param out 输出流 + * @return 复制的字节数 + * @throws IOException 读取异常 + */ + public long copy(OutputStream out) throws IOException { + long count = 0; + while (true) { + byte b = readByte(); + if (isBoundary(b)) { + break; + } + out.write(b); + count++; + } + return count; + } + + /** + * 复制字节流到out, 大于maxBytes或者文件末尾停止 + * + * @param out 输出流 + * @param limit 最大字节数 + * @return 复制的字节数 + * @throws IOException 读取异常 + */ + public long copy(OutputStream out, long limit) throws IOException { + long count = 0; + while (true) { + byte b = readByte(); + if (isBoundary(b)) { + break; + } + out.write(b); + count++; + if (count > limit) { + break; + } + } + return count; + } + + /** + * 跳过边界表示 + * + * @return 跳过的字节数 + * @throws IOException 读取异常 + */ + public long skipToBoundary() throws IOException { + long count = 0; + while (true) { + byte b = readByte(); + count++; + if (isBoundary(b)) { + break; + } + } + return count; + } + + /** + * @param b byte + * @return 是否为边界的标志 + * @throws IOException 读取异常 + */ + public boolean isBoundary(byte b) throws IOException { + int boundaryLen = boundary.length; + mark(boundaryLen + 1); + int bpos = 0; + while (b == boundary[bpos]) { + b = readByte(); + bpos++; + if (bpos == boundaryLen) { + return true; // boundary found! + } + } + reset(); + return false; + } +} diff --git a/src/main/java/cn/hutool/core/net/multipart/UploadFile.java b/src/main/java/cn/hutool/core/net/multipart/UploadFile.java new file mode 100644 index 0000000..7f6887f --- /dev/null +++ b/src/main/java/cn/hutool/core/net/multipart/UploadFile.java @@ -0,0 +1,272 @@ +package cn.hutool.core.net.multipart; + +import cn.hutool.core.io.FileUtil; +import cn.hutool.core.io.IoUtil; +import cn.hutool.core.util.StrUtil; + +import java.io.BufferedOutputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.NoSuchFileException; + +/** + * 上传的文件对象 + * + * @author xiaoleilu + */ +public class UploadFile { + + private static final String TMP_FILE_PREFIX = "hutool-"; + private static final String TMP_FILE_SUFFIX = ".upload.tmp"; + + private final UploadFileHeader header; + private final UploadSetting setting; + + private long size = -1; + + // 文件流(小文件位于内存中) + private byte[] data; + // 临时文件(大文件位于临时文件夹中) + private File tempFile; + + /** + * 构造 + * + * @param header 头部信息 + * @param setting 上传设置 + */ + public UploadFile(UploadFileHeader header, UploadSetting setting) { + this.header = header; + this.setting = setting; + } + + // ---------------------------------------------------------------- operations + + /** + * 从磁盘或者内存中删除这个文件 + */ + public void delete() { + if (tempFile != null) { + //noinspection ResultOfMethodCallIgnored + tempFile.delete(); + } + if (data != null) { + data = null; + } + } + + /** + * 将上传的文件写入指定的目标文件路径,自动创建文件
+ * 写入后原临时文件会被删除 + * + * @param destPath 目标文件路径 + * @return 目标文件 + * @throws IOException IO异常 + */ + public File write(String destPath) throws IOException { + if (data != null || tempFile != null) { + return write(FileUtil.file(destPath)); + } + return null; + } + + /** + * 将上传的文件写入目标文件
+ * 写入后原临时文件会被删除 + * + * @param destination 目标文件 + * @return 目标文件 + * @throws IOException IO异常 + */ + public File write(File destination) throws IOException { + assertValid(); + + if (destination.isDirectory()) { + destination = new File(destination, this.header.getFileName()); + } + if (data != null) { + // 内存中 + FileUtil.writeBytes(data, destination); + data = null; + } else { + // 临时文件 + if(null == this.tempFile){ + throw new NullPointerException("Temp file is null !"); + } + if(!this.tempFile.exists()){ + throw new NoSuchFileException("Temp file: [" + this.tempFile.getAbsolutePath() + "] not exist!"); + } + + FileUtil.move(tempFile, destination, true); + } + return destination; + } + + /** + * @return 获得文件字节流 + * @throws IOException IO异常 + */ + public byte[] getFileContent() throws IOException { + assertValid(); + + if (data != null) { + return data; + } + if (tempFile != null) { + return FileUtil.readBytes(tempFile); + } + return null; + } + + /** + * @return 获得文件流 + * @throws IOException IO异常 + */ + public InputStream getFileInputStream() throws IOException { + assertValid(); + + if (data != null) { + return IoUtil.toBuffered(IoUtil.toStream(this.data)); + } + if (tempFile != null) { + return IoUtil.toBuffered(IoUtil.toStream(this.tempFile)); + } + return null; + } + + // ---------------------------------------------------------------- header + + /** + * @return 上传文件头部信息 + */ + public UploadFileHeader getHeader() { + return header; + } + + /** + * @return 文件名 + */ + public String getFileName() { + return header == null ? null : header.getFileName(); + } + + // ---------------------------------------------------------------- properties + + /** + * @return 上传文件的大小,> 0 表示未上传 + */ + public long size() { + return size; + } + + /** + * @return 是否上传成功 + */ + public boolean isUploaded() { + return size > 0; + } + + /** + * @return 文件是否在内存中 + */ + public boolean isInMemory() { + return data != null; + } + + // ---------------------------------------------------------------- process + + /** + * 处理上传表单流,提取出文件 + * + * @param input 上传表单的流 + * @return 是否成功 + * @throws IOException IO异常 + */ + protected boolean processStream(MultipartRequestInputStream input) throws IOException { + if (!isAllowedExtension()) { + // 非允许的扩展名 + size = input.skipToBoundary(); + return false; + } + size = 0; + + // 处理内存文件 + int memoryThreshold = setting.memoryThreshold; + if (memoryThreshold > 0) { + final ByteArrayOutputStream baos = new ByteArrayOutputStream(memoryThreshold); + final long written = input.copy(baos, memoryThreshold); + data = baos.toByteArray(); + if (written <= memoryThreshold) { + // 文件存放于内存 + size = data.length; + return true; + } + } + + // 处理硬盘文件 + tempFile = FileUtil.createTempFile(TMP_FILE_PREFIX, TMP_FILE_SUFFIX, FileUtil.touch(setting.tmpUploadPath), false); + final BufferedOutputStream out = FileUtil.getOutputStream(this.tempFile); + if (data != null) { + size = data.length; + out.write(data); + data = null; // not needed anymore + } + final long maxFileSize = setting.maxFileSize; + try { + if (maxFileSize == -1) { + size += input.copy(out); + return true; + } + size += input.copy(out, maxFileSize - size + 1); // one more byte to detect larger files + if (size > maxFileSize) { + // 超出上传大小限制 + //noinspection ResultOfMethodCallIgnored + tempFile.delete(); + tempFile = null; + input.skipToBoundary(); + return false; + } + } finally { + IoUtil.close(out); + } + return true; + } + + // ---------------------------------------------------------------------------- Private method start + + /** + * @return 是否为允许的扩展名 + */ + private boolean isAllowedExtension() { + final String[] exts = setting.fileExts; + boolean isAllow = setting.isAllowFileExts; + if (exts == null || exts.length == 0) { + // 如果给定扩展名列表为空,当允许扩展名时全部允许,否则全部禁止 + return isAllow; + } + + final String fileNameExt = FileUtil.extName(this.getFileName()); + for (String fileExtension : setting.fileExts) { + if (fileNameExt.equalsIgnoreCase(fileExtension)) { + return isAllow; + } + } + + // 未匹配到扩展名,如果为允许列表,返回false, 否则true + return !isAllow; + } + + /** + * 断言是否文件流可用 + * + * @throws IOException IO异常 + */ + private void assertValid() throws IOException { + if (!isUploaded()) { + throw new IOException(StrUtil.format("File [{}] upload fail", getFileName())); + } + } + // ---------------------------------------------------------------------------- Private method end +} diff --git a/src/main/java/cn/hutool/core/net/multipart/UploadFileHeader.java b/src/main/java/cn/hutool/core/net/multipart/UploadFileHeader.java new file mode 100644 index 0000000..c04b649 --- /dev/null +++ b/src/main/java/cn/hutool/core/net/multipart/UploadFileHeader.java @@ -0,0 +1,204 @@ +package cn.hutool.core.net.multipart; + +import cn.hutool.core.io.FileUtil; +import cn.hutool.core.util.StrUtil; + +/** + * 上传的文件的头部信息
+ * 来自Jodd + * + * @author jodd.org + */ +public class UploadFileHeader { + + // String dataHeader; + String formFieldName; + + String formFileName; + String path; + String fileName; + + boolean isFile; + String contentType; + String mimeType; + String mimeSubtype; + String contentDisposition; + + UploadFileHeader(String dataHeader) { + processHeaderString(dataHeader); + } + + // ---------------------------------------------------------------- public interface + + /** + * Returns {@code true} if uploaded data are correctly marked as a file.
+ * This is true if header contains string 'filename'. + * + * @return 是否为文件 + */ + public boolean isFile() { + return isFile; + } + + /** + * 返回表单字段名 + * + * @return 表单字段名 + */ + public String getFormFieldName() { + return formFieldName; + } + + /** + * 返回表单中的文件名,来自客户端传入 + * + * @return 表单文件名 + */ + public String getFormFileName() { + return formFileName; + } + + /** + * 获取文件名,不包括路径 + * + * @return 文件名 + */ + public String getFileName() { + return fileName; + } + + /** + * Returns uploaded content type. It is usually in the following form:
+ * mime_type/mime_subtype. + * + * @return content type + * @see #getMimeType() + * @see #getMimeSubtype() + */ + public String getContentType() { + return contentType; + } + + /** + * Returns file types MIME. + * + * @return types MIME + */ + public String getMimeType() { + return mimeType; + } + + /** + * Returns file sub type MIME. + * + * @return sub type MIME + */ + public String getMimeSubtype() { + return mimeSubtype; + } + + /** + * Returns content disposition. Usually it is 'form-data'. + * + * @return content disposition + */ + public String getContentDisposition() { + return contentDisposition; + } + + // ---------------------------------------------------------------- Private Method + + /** + * 获得头信息字符串字符串中指定的值 + * + * @param dataHeader 头信息 + * @param fieldName 字段名 + * @return 字段值 + */ + private String getDataFieldValue(String dataHeader, String fieldName) { + String value = null; + String token = StrUtil.format("{}=\"", fieldName); + int pos = dataHeader.indexOf(token); + if (pos > 0) { + int start = pos + token.length(); + int end = dataHeader.indexOf('"', start); + if ((start > 0) && (end > 0)) { + value = dataHeader.substring(start, end); + } + } + return value; + } + + /** + * 头信息中获得content type + * + * @param dataHeader data header string + * @return content type or an empty string if no content type defined + */ + private String getContentType(String dataHeader) { + String token = "Content-Type:"; + int start = dataHeader.indexOf(token); + if (start == -1) { + return StrUtil.EMPTY; + } + start += token.length(); + return dataHeader.substring(start); + } + + private String getContentDisposition(String dataHeader) { + int start = dataHeader.indexOf(':') + 1; + int end = dataHeader.indexOf(';'); + return dataHeader.substring(start, end); + } + + private String getMimeType(String ContentType) { + int pos = ContentType.indexOf('/'); + if (pos == -1) { + return ContentType; + } + return ContentType.substring(1, pos); + } + + private String getMimeSubtype(String ContentType) { + int start = ContentType.indexOf('/'); + if (start == -1) { + return ContentType; + } + start++; + return ContentType.substring(start); + } + + /** + * 处理头字符串,使之转化为字段 + * + * @param dataHeader 头字符串 + */ + private void processHeaderString(String dataHeader) { + isFile = dataHeader.indexOf("filename") > 0; + formFieldName = getDataFieldValue(dataHeader, "name"); + if (isFile) { + formFileName = getDataFieldValue(dataHeader, "filename"); + if (formFileName == null) { + return; + } + if (formFileName.length() == 0) { + path = StrUtil.EMPTY; + fileName = StrUtil.EMPTY; + } + int ls = FileUtil.lastIndexOfSeparator(formFileName); + if (ls == -1) { + path = StrUtil.EMPTY; + fileName = formFileName; + } else { + path = formFileName.substring(0, ls); + fileName = formFileName.substring(ls); + } + if (fileName.length() > 0) { + this.contentType = getContentType(dataHeader); + mimeType = getMimeType(contentType); + mimeSubtype = getMimeSubtype(contentType); + contentDisposition = getContentDisposition(dataHeader); + } + } + } +} diff --git a/src/main/java/cn/hutool/core/net/multipart/UploadSetting.java b/src/main/java/cn/hutool/core/net/multipart/UploadSetting.java new file mode 100644 index 0000000..9f42778 --- /dev/null +++ b/src/main/java/cn/hutool/core/net/multipart/UploadSetting.java @@ -0,0 +1,110 @@ +package cn.hutool.core.net.multipart; + +/** + * 上传文件设定文件 + * + * @author xiaoleilu + * + */ +public class UploadSetting { + + /** 最大文件大小,默认无限制 */ + protected long maxFileSize = -1; + /** 文件保存到内存的边界 */ + protected int memoryThreshold = 8192; + /** 临时文件目录 */ + protected String tmpUploadPath; + /** 文件扩展名限定 */ + protected String[] fileExts; + /** 扩展名是允许列表还是禁止列表 */ + protected boolean isAllowFileExts = true; + + public UploadSetting() { + } + + // ---------------------------------------------------------------------- Setters and Getters start + /** + * @return 获得最大文件大小,-1表示无限制 + */ + public long getMaxFileSize() { + return maxFileSize; + } + + /** + * 设定最大文件大小,-1表示无限制 + * + * @param maxFileSize 最大文件大小 + */ + public void setMaxFileSize(long maxFileSize) { + this.maxFileSize = maxFileSize; + } + + /** + * @return 文件保存到内存的边界 + */ + public int getMemoryThreshold() { + return memoryThreshold; + } + + /** + * 设定文件保存到内存的边界
+ * 如果文件大小小于这个边界,将保存于内存中,否则保存至临时目录中 + * + * @param memoryThreshold 文件保存到内存的边界 + */ + public void setMemoryThreshold(int memoryThreshold) { + this.memoryThreshold = memoryThreshold; + } + + /** + * @return 上传文件的临时目录,若为空,使用系统目录 + */ + public String getTmpUploadPath() { + return tmpUploadPath; + } + + /** + * 设定上传文件的临时目录,null表示使用系统临时目录 + * + * @param tmpUploadPath 临时目录,绝对路径 + */ + public void setTmpUploadPath(String tmpUploadPath) { + this.tmpUploadPath = tmpUploadPath; + } + + /** + * @return 文件扩展名限定列表 + */ + public String[] getFileExts() { + return fileExts; + } + + /** + * 设定文件扩展名限定里列表
+ * 禁止列表还是允许列表取决于isAllowFileExts + * + * @param fileExts 文件扩展名列表 + */ + public void setFileExts(String[] fileExts) { + this.fileExts = fileExts; + } + + /** + * 是否允许文件扩展名
+ * + * @return 若true表示只允许列表里的扩展名,否则是禁止列表里的扩展名 + */ + public boolean isAllowFileExts() { + return isAllowFileExts; + } + + /** + * 设定是否允许扩展名 + * + * @param isAllowFileExts 若true表示只允许列表里的扩展名,否则是禁止列表里的扩展名 + */ + public void setAllowFileExts(boolean isAllowFileExts) { + this.isAllowFileExts = isAllowFileExts; + } + // ---------------------------------------------------------------------- Setters and Getters end +} diff --git a/src/main/java/cn/hutool/core/net/multipart/package-info.java b/src/main/java/cn/hutool/core/net/multipart/package-info.java new file mode 100644 index 0000000..630081c --- /dev/null +++ b/src/main/java/cn/hutool/core/net/multipart/package-info.java @@ -0,0 +1,7 @@ +/** + * 文件上传封装 + * + * @author looly + * + */ +package cn.hutool.core.net.multipart; \ No newline at end of file diff --git a/src/main/java/cn/hutool/core/net/package-info.java b/src/main/java/cn/hutool/core/net/package-info.java new file mode 100644 index 0000000..a1f6dcb --- /dev/null +++ b/src/main/java/cn/hutool/core/net/package-info.java @@ -0,0 +1,7 @@ +/** + * 网络相关工具 + * + * @author looly + * + */ +package cn.hutool.core.net; \ No newline at end of file diff --git a/src/main/java/cn/hutool/core/net/url/UrlBuilder.java b/src/main/java/cn/hutool/core/net/url/UrlBuilder.java new file mode 100644 index 0000000..5725991 --- /dev/null +++ b/src/main/java/cn/hutool/core/net/url/UrlBuilder.java @@ -0,0 +1,583 @@ +package cn.hutool.core.net.url; + +import cn.hutool.core.builder.Builder; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.net.RFC3986; +import cn.hutool.core.util.CharsetUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.core.util.URLUtil; + +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.net.URLStreamHandler; +import java.nio.charset.Charset; + +/** + * URL 生成器,格式形如: + *

+ * [scheme:]scheme-specific-part[#fragment]
+ * [scheme:][//authority][path][?query][#fragment]
+ * [scheme:][//host:port][path][?query][#fragment]
+ * 
+ * + * @author looly + * @see Uniform Resource Identifier + * @since 5.3.1 + */ +public final class UrlBuilder implements Builder { + private static final long serialVersionUID = 1L; + private static final String DEFAULT_SCHEME = "http"; + + /** + * 协议,例如http + */ + private String scheme; + /** + * 主机,例如127.0.0.1 + */ + private String host; + /** + * 端口,默认-1 + */ + private int port = -1; + /** + * 路径,例如/aa/bb/cc + */ + private UrlPath path; + /** + * 查询语句,例如a=1&b=2 + */ + private UrlQuery query; + /** + * 标识符,例如#后边的部分 + */ + private String fragment; + + /** + * 编码,用于URLEncode和URLDecode + */ + private Charset charset; + /** + * 是否需要编码`%`
+ * 区别对待,如果是,则生成URL时需要重新全部编码,否则跳过所有`%` + */ + private boolean needEncodePercent; + + /** + * 使用URI构建UrlBuilder + * + * @param uri URI + * @param charset 编码,用于URLEncode和URLDecode + * @return UrlBuilder + */ + public static UrlBuilder of(URI uri, Charset charset) { + return of(uri.getScheme(), uri.getHost(), uri.getPort(), uri.getPath(), uri.getRawQuery(), uri.getFragment(), charset); + } + + /** + * 使用URL字符串构建UrlBuilder,当传入的URL没有协议时,按照http协议对待
+ * 此方法不对URL编码 + * + * @param httpUrl URL字符串 + * @return UrlBuilder + * @since 5.4.3 + */ + public static UrlBuilder ofHttpWithoutEncode(String httpUrl) { + return ofHttp(httpUrl, null); + } + + /** + * 使用URL字符串构建UrlBuilder,当传入的URL没有协议时,按照http协议对待,编码默认使用UTF-8 + * + * @param httpUrl URL字符串 + * @return UrlBuilder + * @since 5.6.3 + */ + public static UrlBuilder ofHttp(String httpUrl) { + return ofHttp(httpUrl, CharsetUtil.CHARSET_UTF_8); + } + + /** + * 使用URL字符串构建UrlBuilder,当传入的URL没有协议时,按照http协议对待。 + * + * @param httpUrl URL字符串 + * @param charset 编码,用于URLEncode和URLDecode + * @return UrlBuilder + */ + public static UrlBuilder ofHttp(String httpUrl, Charset charset) { + Assert.notBlank(httpUrl, "Http url must be not blank!"); + httpUrl = StrUtil.trimStart(httpUrl); + // issue#I66CIR + if(!StrUtil.startWithAnyIgnoreCase(httpUrl, "http://", "https://")){ + httpUrl = "http://" + httpUrl; + } + return of(httpUrl, charset); + } + + /** + * 使用URL字符串构建UrlBuilder,默认使用UTF-8编码 + * + * @param url URL字符串 + * @return UrlBuilder + */ + public static UrlBuilder of(String url) { + return of(url, CharsetUtil.CHARSET_UTF_8); + } + + /** + * 使用URL字符串构建UrlBuilder + * + * @param url URL字符串 + * @param charset 编码,用于URLEncode和URLDecode + * @return UrlBuilder + */ + public static UrlBuilder of(String url, Charset charset) { + Assert.notBlank(url, "Url must be not blank!"); + return of(URLUtil.url(StrUtil.trim(url)), charset); + } + + /** + * 使用URL构建UrlBuilder + * + * @param url URL + * @param charset 编码,用于URLEncode和URLDecode + * @return UrlBuilder + */ + public static UrlBuilder of(URL url, Charset charset) { + return of(url.getProtocol(), url.getHost(), url.getPort(), url.getPath(), url.getQuery(), url.getRef(), charset); + } + + /** + * 构建UrlBuilder + * + * @param scheme 协议,默认http + * @param host 主机,例如127.0.0.1 + * @param port 端口,-1表示默认端口 + * @param path 路径,例如/aa/bb/cc + * @param query 查询,例如a=1&b=2 + * @param fragment 标识符例如#后边的部分 + * @param charset 编码,用于URLEncode和URLDecode + * @return UrlBuilder + */ + public static UrlBuilder of(String scheme, String host, int port, String path, String query, String fragment, Charset charset) { + return of(scheme, host, port, + UrlPath.of(path, charset), + UrlQuery.of(query, charset, false), fragment, charset); + } + + /** + * 构建UrlBuilder + * + * @param scheme 协议,默认http + * @param host 主机,例如127.0.0.1 + * @param port 端口,-1表示默认端口 + * @param path 路径,例如/aa/bb/cc + * @param query 查询,例如a=1&b=2 + * @param fragment 标识符例如#后边的部分 + * @param charset 编码,用于URLEncode和URLDecode + * @return UrlBuilder + */ + public static UrlBuilder of(String scheme, String host, int port, UrlPath path, UrlQuery query, String fragment, Charset charset) { + return new UrlBuilder(scheme, host, port, path, query, fragment, charset); + } + + /** + * 创建空的UrlBuilder + * + * @return UrlBuilder + * @deprecated 请使用 {@link #of()} + */ + @Deprecated + public static UrlBuilder create() { + return new UrlBuilder(); + } + + /** + * 创建空的UrlBuilder + * + * @return UrlBuilder + */ + public static UrlBuilder of() { + return new UrlBuilder(); + } + + /** + * 构造 + */ + public UrlBuilder() { + this.charset = CharsetUtil.CHARSET_UTF_8; + } + + /** + * 构造 + * + * @param scheme 协议,默认http + * @param host 主机,例如127.0.0.1 + * @param port 端口,-1表示默认端口 + * @param path 路径,例如/aa/bb/cc + * @param query 查询,例如a=1&b=2 + * @param fragment 标识符例如#后边的部分 + * @param charset 编码,用于URLEncode和URLDecode,{@code null}表示不编码 + */ + public UrlBuilder(String scheme, String host, int port, UrlPath path, UrlQuery query, String fragment, Charset charset) { + this.charset = charset; + this.scheme = scheme; + this.host = host; + this.port = port; + this.path = path; + this.query = query; + this.setFragment(fragment); + // 编码非空情况下做解码 + this.needEncodePercent = null != charset; + } + + /** + * 获取协议,例如http + * + * @return 协议,例如http + */ + public String getScheme() { + return scheme; + } + + /** + * 获取协议,例如http,如果用户未定义协议,使用默认的http协议 + * + * @return 协议,例如http + */ + public String getSchemeWithDefault() { + return StrUtil.emptyToDefault(this.scheme, DEFAULT_SCHEME); + } + + /** + * 设置协议,例如http + * + * @param scheme 协议,例如http + * @return this + */ + public UrlBuilder setScheme(String scheme) { + this.scheme = scheme; + return this; + } + + /** + * 获取 主机,例如127.0.0.1 + * + * @return 主机,例如127.0.0.1 + */ + public String getHost() { + return host; + } + + /** + * 设置主机,例如127.0.0.1 + * + * @param host 主机,例如127.0.0.1 + * @return this + */ + public UrlBuilder setHost(String host) { + this.host = host; + return this; + } + + /** + * 获取端口,默认-1 + * + * @return 端口,默认-1 + */ + public int getPort() { + return port; + } + + /** + * 获取端口,如果未自定义返回协议默认端口 + * + * @return 端口 + */ + public int getPortWithDefault() { + int port = getPort(); + if (port > 0) { + return port; + } + URL url = this.toURL(); + return url.getDefaultPort(); + } + + /** + * 设置端口,默认-1 + * + * @param port 端口,默认-1 + * @return this + */ + public UrlBuilder setPort(int port) { + this.port = port; + return this; + } + + /** + * 获得authority部分 + * + * @return authority部分 + */ + public String getAuthority() { + return (port < 0) ? host : host + ":" + port; + } + + /** + * 获取路径,例如/aa/bb/cc + * + * @return 路径,例如/aa/bb/cc + */ + public UrlPath getPath() { + return path; + } + + /** + * 获得路径,例如/aa/bb/cc + * + * @return 路径,例如/aa/bb/cc + */ + public String getPathStr() { + return null == this.path ? StrUtil.SLASH : this.path.build(charset, this.needEncodePercent); + } + + /** + * 是否path的末尾加 / + * + * @param withEngTag 是否path的末尾加 / + * @return this + * @since 5.8.5 + */ + public UrlBuilder setWithEndTag(boolean withEngTag) { + if (null == this.path) { + this.path = new UrlPath(); + } + + this.path.setWithEndTag(withEngTag); + return this; + } + + /** + * 设置路径,例如/aa/bb/cc,将覆盖之前所有的path相关设置 + * + * @param path 路径,例如/aa/bb/cc + * @return this + */ + public UrlBuilder setPath(UrlPath path) { + this.path = path; + return this; + } + + /** + * 增加路径,在现有路径基础上追加路径 + * + * @param path 路径,例如aaa/bbb/ccc + * @return this + */ + public UrlBuilder addPath(CharSequence path) { + UrlPath.of(path, this.charset).getSegments().forEach(this::addPathSegment); + return this; + } + + /** + * 增加路径节点,路径节点中的"/"会被转义为"%2F" + * + * @param segment 路径节点 + * @return this + * @since 5.7.16 + */ + public UrlBuilder addPathSegment(CharSequence segment) { + if (StrUtil.isEmpty(segment)) { + return this; + } + if (null == this.path) { + this.path = new UrlPath(); + } + this.path.add(segment); + return this; + } + + /** + * 追加path节点 + * + * @param path path节点 + * @return this + * @deprecated 方法重复,请使用{@link #addPath(CharSequence)} + */ + @Deprecated + public UrlBuilder appendPath(CharSequence path) { + return addPath(path); + } + + /** + * 获取查询语句,例如a=1&b=2
+ * 可能为{@code null} + * + * @return 查询语句,例如a=1&b=2,可能为{@code null} + */ + public UrlQuery getQuery() { + return query; + } + + /** + * 获取查询语句,例如a=1&b=2 + * + * @return 查询语句,例如a=1&b=2 + */ + public String getQueryStr() { + return null == this.query ? null : this.query.build(this.charset, this.needEncodePercent); + } + + /** + * 设置查询语句,例如a=1&b=2,将覆盖之前所有的query相关设置 + * + * @param query 查询语句,例如a=1&b=2 + * @return this + */ + public UrlBuilder setQuery(UrlQuery query) { + this.query = query; + return this; + } + + /** + * 添加查询项,支持重复键 + * + * @param key 键 + * @param value 值 + * @return this + */ + public UrlBuilder addQuery(String key, Object value) { + if (StrUtil.isEmpty(key)) { + return this; + } + + if (this.query == null) { + this.query = new UrlQuery(); + } + this.query.add(key, value); + return this; + } + + /** + * 获取标识符,#后边的部分 + * + * @return 标识符,例如#后边的部分 + */ + public String getFragment() { + return fragment; + } + + /** + * 获取标识符,#后边的部分 + * + * @return 标识符,例如#后边的部分 + */ + public String getFragmentEncoded() { + final char[] safeChars = this.needEncodePercent ? null : new char[]{'%'}; + return RFC3986.FRAGMENT.encode(this.fragment, this.charset, safeChars); + } + + /** + * 设置标识符,例如#后边的部分 + * + * @param fragment 标识符,例如#后边的部分 + * @return this + */ + public UrlBuilder setFragment(String fragment) { + if (StrUtil.isEmpty(fragment)) { + this.fragment = null; + } + this.fragment = StrUtil.removePrefix(fragment, "#"); + return this; + } + + /** + * 获取编码,用于URLEncode和URLDecode + * + * @return 编码 + */ + public Charset getCharset() { + return charset; + } + + /** + * 设置编码,用于URLEncode和URLDecode + * + * @param charset 编码 + * @return this + */ + public UrlBuilder setCharset(Charset charset) { + this.charset = charset; + return this; + } + + /** + * 创建URL字符串 + * + * @return URL字符串 + */ + @Override + public String build() { + return toURL().toString(); + } + + /** + * 转换为{@link URL} 对象 + * + * @return {@link URL} + */ + public URL toURL() { + return toURL(null); + } + + /** + * 转换为{@link URL} 对象 + * + * @param handler {@link URLStreamHandler},null表示默认 + * @return {@link URL} + */ + public URL toURL(URLStreamHandler handler) { + final StringBuilder fileBuilder = new StringBuilder(); + + // path + fileBuilder.append(getPathStr()); + + // query + final String query = getQueryStr(); + if (StrUtil.isNotBlank(query)) { + fileBuilder.append('?').append(query); + } + + // fragment + if (StrUtil.isNotBlank(this.fragment)) { + fileBuilder.append('#').append(getFragmentEncoded()); + } + + try { + return new URL(getSchemeWithDefault(), host, port, fileBuilder.toString(), handler); + } catch (MalformedURLException e) { + return null; + } + } + + /** + * 转换为URI + * + * @return URI + */ + public URI toURI() { + try { + return toURL().toURI(); + } catch (URISyntaxException e) { + return null; + } + } + + @Override + public String toString() { + return build(); + } + +} diff --git a/src/main/java/cn/hutool/core/net/url/UrlPath.java b/src/main/java/cn/hutool/core/net/url/UrlPath.java new file mode 100644 index 0000000..f4715ad --- /dev/null +++ b/src/main/java/cn/hutool/core/net/url/UrlPath.java @@ -0,0 +1,219 @@ +package cn.hutool.core.net.url; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.collection.ListUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.net.RFC3986; +import cn.hutool.core.net.URLDecoder; +import cn.hutool.core.util.CharUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; + +import java.nio.charset.Charset; +import java.util.LinkedList; +import java.util.List; + +/** + * URL中Path部分的封装 + * + * @author looly + * @since 5.3.1 + */ +public class UrlPath { + + private List segments; + private boolean withEngTag; + + /** + * 构建UrlPath + * + * @param pathStr 初始化的路径字符串 + * @param charset decode用的编码,null表示不做decode + * @return UrlPath + */ + public static UrlPath of(CharSequence pathStr, Charset charset) { + final UrlPath urlPath = new UrlPath(); + urlPath.parse(pathStr, charset); + return urlPath; + } + + /** + * 是否path的末尾加 / + * + * @param withEngTag 是否path的末尾加 / + * @return this + */ + public UrlPath setWithEndTag(boolean withEngTag) { + this.withEngTag = withEngTag; + return this; + } + + /** + * 获取path的节点列表 + * + * @return 节点列表 + */ + public List getSegments() { + return ObjectUtil.defaultIfNull(this.segments, ListUtil.empty()); + } + + /** + * 获得指定节点 + * + * @param index 节点位置 + * @return 节点,无节点或者越界返回null + */ + public String getSegment(int index) { + if (null == this.segments || index >= this.segments.size()) { + return null; + } + return this.segments.get(index); + } + + /** + * 添加到path最后面 + * + * @param segment Path节点 + * @return this + */ + public UrlPath add(CharSequence segment) { + addInternal(fixPath(segment), false); + return this; + } + + /** + * 添加到path最前面 + * + * @param segment Path节点 + * @return this + */ + public UrlPath addBefore(CharSequence segment) { + addInternal(fixPath(segment), true); + return this; + } + + /** + * 解析path + * + * @param path 路径,类似于aaa/bb/ccc或/aaa/bbb/ccc + * @param charset decode编码,null表示不解码 + * @return this + */ + public UrlPath parse(CharSequence path, Charset charset) { + if (StrUtil.isNotEmpty(path)) { + // 原URL中以/结尾,则这个规则需保留,issue#I1G44J@Gitee + if(StrUtil.endWith(path, CharUtil.SLASH)){ + this.withEngTag = true; + } + + path = fixPath(path); + if(StrUtil.isNotEmpty(path)){ + final List split = StrUtil.split(path, '/'); + for (String seg : split) { + addInternal(URLDecoder.decodeForPath(seg, charset), false); + } + } + } + + return this; + } + + /** + * 构建path,前面带'/'
+ *
+	 *     path = path-abempty / path-absolute / path-noscheme / path-rootless / path-empty
+	 * 
+ * + * @param charset encode编码,null表示不做encode + * @return 如果没有任何内容,则返回空字符串"" + */ + public String build(Charset charset) { + return build(charset, true); + } + + /** + * 构建path,前面带'/'
+ *
+	 *     path = path-abempty / path-absolute / path-noscheme / path-rootless / path-empty
+	 * 
+ * + * @param charset encode编码,null表示不做encode + * @param encodePercent 是否编码`%` + * @return 如果没有任何内容,则返回空字符串"" + * @since 5.8.0 + */ + public String build(Charset charset, boolean encodePercent) { + if (CollUtil.isEmpty(this.segments)) { + // 没有节点的path取决于是否末尾追加/,如果不追加返回空串,否则返回/ + return withEngTag ? StrUtil.SLASH : StrUtil.EMPTY; + } + + final char[] safeChars = encodePercent ? null : new char[]{'%'}; + final StringBuilder builder = new StringBuilder(); + for (final String segment : segments) { + if(builder.length() == 0){ + // 根据https://www.ietf.org/rfc/rfc3986.html#section-3.3定义 + // path的第一部分不允许有":",其余部分允许 + // 在此处的Path部分特指host之后的部分,即不包含第一部分 + builder.append(CharUtil.SLASH).append(RFC3986.SEGMENT_NZ_NC.encode(segment, charset, safeChars)); + } else { + builder.append(CharUtil.SLASH).append(RFC3986.SEGMENT.encode(segment, charset, safeChars)); + } + } + + if(withEngTag){ + if (StrUtil.isEmpty(builder)) { + // 空白追加是保证以/开头 + builder.append(CharUtil.SLASH); + }else if (!StrUtil.endWith(builder, CharUtil.SLASH)) { + // 尾部没有/则追加,否则不追加 + builder.append(CharUtil.SLASH); + } + } + + return builder.toString(); + } + + @Override + public String toString() { + return build(null); + } + + /** + * 增加节点 + * + * @param segment 节点 + * @param before 是否在前面添加 + */ + private void addInternal(CharSequence segment, boolean before) { + if (this.segments == null) { + this.segments = new LinkedList<>(); + } + + final String seg = StrUtil.str(segment); + if (before) { + this.segments.add(0, seg); + } else { + this.segments.add(seg); + } + } + + /** + * 修正路径,包括去掉前后的/,去掉空白符 + * + * @param path 节点或路径path + * @return 修正后的路径 + */ + private static String fixPath(CharSequence path) { + Assert.notNull(path, "Path segment must be not null!"); + if ("/".contentEquals(path)) { + return StrUtil.EMPTY; + } + + String segmentStr = StrUtil.trim(path); + segmentStr = StrUtil.removePrefix(segmentStr, StrUtil.SLASH); + segmentStr = StrUtil.removeSuffix(segmentStr, StrUtil.SLASH); + segmentStr = StrUtil.trim(segmentStr); + return segmentStr; + } +} diff --git a/src/main/java/cn/hutool/core/net/url/UrlQuery.java b/src/main/java/cn/hutool/core/net/url/UrlQuery.java new file mode 100644 index 0000000..6a59c64 --- /dev/null +++ b/src/main/java/cn/hutool/core/net/url/UrlQuery.java @@ -0,0 +1,415 @@ +package cn.hutool.core.net.url; + +import cn.hutool.core.codec.PercentCodec; +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.collection.IterUtil; +import cn.hutool.core.convert.Convert; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.map.TableMap; +import cn.hutool.core.net.FormUrlencoded; +import cn.hutool.core.net.RFC3986; +import cn.hutool.core.net.URLDecoder; +import cn.hutool.core.util.StrUtil; + +import java.nio.charset.Charset; +import java.util.Iterator; +import java.util.Map; + +/** + * URL中查询字符串部分的封装,类似于: + *
+ *   key1=v1&key2=&key3=v3
+ * 
+ * 查询封装分为解析查询字符串和构建查询字符串,解析可通过charset为null来自定义是否decode编码后的内容,
+ * 构建则通过charset是否为null是否encode参数键值对 + * + * @author looly + * @since 5.3.1 + */ +public class UrlQuery { + + private final TableMap query; + /** + * 是否为x-www-form-urlencoded模式,此模式下空格会编码为'+' + */ + private final boolean isFormUrlEncoded; + + /** + * 构建UrlQuery + * + * @param queryMap 初始化的查询键值对 + * @return UrlQuery + */ + public static UrlQuery of(Map queryMap) { + return new UrlQuery(queryMap); + } + + /** + * 构建UrlQuery + * + * @param queryMap 初始化的查询键值对 + * @param isFormUrlEncoded 是否为x-www-form-urlencoded模式,此模式下空格会编码为'+' + * @return UrlQuery + */ + public static UrlQuery of(Map queryMap, boolean isFormUrlEncoded) { + return new UrlQuery(queryMap, isFormUrlEncoded); + } + + /** + * 构建UrlQuery + * + * @param queryStr 初始化的查询字符串 + * @param charset decode用的编码,null表示不做decode + * @return UrlQuery + */ + public static UrlQuery of(String queryStr, Charset charset) { + return of(queryStr, charset, true); + } + + /** + * 构建UrlQuery + * + * @param queryStr 初始化的查询字符串 + * @param charset decode用的编码,null表示不做decode + * @param autoRemovePath 是否自动去除path部分,{@code true}则自动去除第一个?前的内容 + * @return UrlQuery + * @since 5.5.8 + */ + public static UrlQuery of(String queryStr, Charset charset, boolean autoRemovePath) { + return of(queryStr, charset, autoRemovePath, false); + } + + /** + * 构建UrlQuery + * + * @param queryStr 初始化的查询字符串 + * @param charset decode用的编码,null表示不做decode + * @param autoRemovePath 是否自动去除path部分,{@code true}则自动去除第一个?前的内容 + * @param isFormUrlEncoded 是否为x-www-form-urlencoded模式,此模式下空格会编码为'+' + * @return UrlQuery + * @since 5.7.16 + */ + public static UrlQuery of(String queryStr, Charset charset, boolean autoRemovePath, boolean isFormUrlEncoded) { + return new UrlQuery(isFormUrlEncoded).parse(queryStr, charset, autoRemovePath); + } + + /** + * 构造 + */ + public UrlQuery() { + this(null); + } + + /** + * 构造 + * + * @param isFormUrlEncoded 是否为x-www-form-urlencoded模式,此模式下空格会编码为'+' + * @since 5.7.16 + */ + public UrlQuery(boolean isFormUrlEncoded) { + this(null, isFormUrlEncoded); + } + + /** + * 构造 + * + * @param queryMap 初始化的查询键值对 + */ + public UrlQuery(Map queryMap) { + this(queryMap, false); + } + + /** + * 构造 + * + * @param queryMap 初始化的查询键值对 + * @param isFormUrlEncoded 是否为x-www-form-urlencoded模式,此模式下空格会编码为'+' + * @since 5.7.16 + */ + public UrlQuery(Map queryMap, boolean isFormUrlEncoded) { + if (MapUtil.isNotEmpty(queryMap)) { + query = new TableMap<>(queryMap.size()); + addAll(queryMap); + } else { + query = new TableMap<>(MapUtil.DEFAULT_INITIAL_CAPACITY); + } + this.isFormUrlEncoded = isFormUrlEncoded; + } + + /** + * 增加键值对 + * + * @param key 键 + * @param value 值,集合和数组转换为逗号分隔形式 + * @return this + */ + public UrlQuery add(CharSequence key, Object value) { + this.query.put(key, toStr(value)); + return this; + } + + /** + * 批量增加键值对 + * + * @param queryMap query中的键值对 + * @return this + */ + public UrlQuery addAll(Map queryMap) { + if (MapUtil.isNotEmpty(queryMap)) { + queryMap.forEach(this::add); + } + return this; + } + + /** + * 解析URL中的查询字符串 + * + * @param queryStr 查询字符串,类似于key1=v1&key2=&key3=v3 + * @param charset decode编码,null表示不做decode + * @return this + */ + public UrlQuery parse(String queryStr, Charset charset) { + return parse(queryStr, charset, true); + } + + /** + * 解析URL中的查询字符串 + * + * @param queryStr 查询字符串,类似于key1=v1&key2=&key3=v3 + * @param charset decode编码,null表示不做decode + * @param autoRemovePath 是否自动去除path部分,{@code true}则自动去除第一个?前的内容 + * @return this + * @since 5.5.8 + */ + public UrlQuery parse(String queryStr, Charset charset, boolean autoRemovePath) { + if (StrUtil.isBlank(queryStr)) { + return this; + } + + if (autoRemovePath) { + // 去掉Path部分 + int pathEndPos = queryStr.indexOf('?'); + if (pathEndPos > -1) { + queryStr = StrUtil.subSuf(queryStr, pathEndPos + 1); + if (StrUtil.isBlank(queryStr)) { + return this; + } + } + } + + return doParse(queryStr, charset); + } + + /** + * 获得查询的Map + * + * @return 查询的Map,只读 + */ + public Map getQueryMap() { + return MapUtil.unmodifiable(this.query); + } + + /** + * 获取查询值 + * + * @param key 键 + * @return 值 + */ + public CharSequence get(CharSequence key) { + if (MapUtil.isEmpty(this.query)) { + return null; + } + return this.query.get(key); + } + + /** + * 构建URL查询字符串,即将key-value键值对转换为{@code key1=v1&key2=v2&key3=v3}形式。
+ * 对于{@code null}处理规则如下: + *
    + *
  • 如果key为{@code null},则这个键值对忽略
  • + *
  • 如果value为{@code null},只保留key,如key1对应value为{@code null}生成类似于{@code key1&key2=v2}形式
  • + *
+ * + * @param charset encode编码,null表示不做encode编码 + * @return URL查询字符串 + */ + public String build(Charset charset) { + return build(charset, true); + } + + /** + * 构建URL查询字符串,即将key-value键值对转换为{@code key1=v1&key2=v2&key3=v3}形式。
+ * 对于{@code null}处理规则如下: + *
    + *
  • 如果key为{@code null},则这个键值对忽略
  • + *
  • 如果value为{@code null},只保留key,如key1对应value为{@code null}生成类似于{@code key1&key2=v2}形式
  • + *
+ * + * @param charset encode编码,null表示不做encode编码 + * @param encodePercent 是否编码`%` + * @return URL查询字符串 + */ + public String build(Charset charset, boolean encodePercent) { + if (isFormUrlEncoded) { + return build(FormUrlencoded.ALL, FormUrlencoded.ALL, charset, encodePercent); + } + + return build(RFC3986.QUERY_PARAM_NAME, RFC3986.QUERY_PARAM_VALUE, charset, encodePercent); + } + + /** + * 构建URL查询字符串,即将key-value键值对转换为{@code key1=v1&key2=v2&key3=v3}形式。
+ * 对于{@code null}处理规则如下: + *
    + *
  • 如果key为{@code null},则这个键值对忽略
  • + *
  • 如果value为{@code null},只保留key,如key1对应value为{@code null}生成类似于{@code key1&key2=v2}形式
  • + *
+ * + * @param keyCoder 键值对中键的编码器 + * @param valueCoder 键值对中值的编码器 + * @param charset encode编码,null表示不做encode编码 + * @return URL查询字符串 + * @since 5.7.16 + */ + public String build(PercentCodec keyCoder, PercentCodec valueCoder, Charset charset) { + return build(keyCoder, valueCoder, charset, true); + } + + /** + * 构建URL查询字符串,即将key-value键值对转换为{@code key1=v1&key2=v2&key3=v3}形式。
+ * 对于{@code null}处理规则如下: + *
    + *
  • 如果key为{@code null},则这个键值对忽略
  • + *
  • 如果value为{@code null},只保留key,如key1对应value为{@code null}生成类似于{@code key1&key2=v2}形式
  • + *
+ * + * @param keyCoder 键值对中键的编码器 + * @param valueCoder 键值对中值的编码器 + * @param charset encode编码,null表示不做encode编码 + * @param encodePercent 是否编码`%` + * @return URL查询字符串 + * @since 5.8.0 + */ + public String build(PercentCodec keyCoder, PercentCodec valueCoder, Charset charset, boolean encodePercent) { + if (MapUtil.isEmpty(this.query)) { + return StrUtil.EMPTY; + } + + final char[] safeChars = encodePercent ? null : new char[]{'%'}; + final StringBuilder sb = new StringBuilder(); + CharSequence name; + CharSequence value; + for (Map.Entry entry : this.query) { + name = entry.getKey(); + if (null != name) { + if (sb.length() > 0) { + sb.append("&"); + } + sb.append(keyCoder.encode(name, charset, safeChars)); + value = entry.getValue(); + if (null != value) { + sb.append("=").append(valueCoder.encode(value, charset, safeChars)); + } + } + } + return sb.toString(); + } + + /** + * 生成查询字符串,类似于aaa=111&bbb=222
+ * 此方法不对任何特殊字符编码,仅用于输出显示 + * + * @return 查询字符串 + */ + @Override + public String toString() { + return build(null); + } + + /** + * 解析URL中的查询字符串
+ * 规则见:https://url.spec.whatwg.org/#urlencoded-parsing + * + * @param queryStr 查询字符串,类似于key1=v1&key2=&key3=v3 + * @param charset decode编码,null表示不做decode + * @return this + * @since 5.5.8 + */ + private UrlQuery doParse(String queryStr, Charset charset) { + final int len = queryStr.length(); + String name = null; + int pos = 0; // 未处理字符开始位置 + int i; // 未处理字符结束位置 + char c; // 当前字符 + for (i = 0; i < len; i++) { + c = queryStr.charAt(i); + switch (c) { + case '='://键和值的分界符 + if (null == name) { + // name可以是"" + name = queryStr.substring(pos, i); + // 开始位置从分节符后开始 + pos = i + 1; + } + // 当=不作为分界符时,按照普通字符对待 + break; + case '&'://键值对之间的分界符 + addParam(name, queryStr.substring(pos, i), charset); + name = null; + if (i + 4 < len && "amp;".equals(queryStr.substring(i + 1, i + 5))) { + // issue#850@Github,"&"转义为"&" + i += 4; + } + // 开始位置从分节符后开始 + pos = i + 1; + break; + } + } + + // 处理结尾 + addParam(name, queryStr.substring(pos, i), charset); + + return this; + } + + /** + * 对象转换为字符串,用于URL的Query中 + * + * @param value 值 + * @return 字符串 + */ + private static String toStr(Object value) { + String result; + if (value instanceof Iterable) { + result = CollUtil.join((Iterable) value, ","); + } else if (value instanceof Iterator) { + result = IterUtil.join((Iterator) value, ","); + } else { + result = Convert.toStr(value); + } + return result; + } + + /** + * 将键值对加入到值为List类型的Map中,,情况如下: + *
+	 *     1、key和value都不为null,类似于 "a=1"或者"=1",直接put
+	 *     2、key不为null,value为null,类似于 "a=",值传""
+	 *     3、key为null,value不为null,类似于 "1"
+	 *     4、key和value都为null,忽略之,比如&&
+	 * 
+ * + * @param key key,为null则value作为key + * @param value value,为null且key不为null时传入"" + * @param charset 编码 + */ + private void addParam(String key, String value, Charset charset) { + if (null != key) { + final String actualKey = URLDecoder.decode(key, charset, isFormUrlEncoded); + this.query.put(actualKey, StrUtil.nullToEmpty(URLDecoder.decode(value, charset, isFormUrlEncoded))); + } else if (null != value) { + // name为空,value作为name,value赋值null + this.query.put(URLDecoder.decode(value, charset, isFormUrlEncoded), null); + } + } +} diff --git a/src/main/java/cn/hutool/core/net/url/package-info.java b/src/main/java/cn/hutool/core/net/url/package-info.java new file mode 100644 index 0000000..d33494c --- /dev/null +++ b/src/main/java/cn/hutool/core/net/url/package-info.java @@ -0,0 +1,7 @@ +/** + * URL相关工具 + * + * @author looly + * @since 5.3.1 + */ +package cn.hutool.core.net.url; \ No newline at end of file diff --git a/src/main/java/cn/hutool/core/package-info.java b/src/main/java/cn/hutool/core/package-info.java new file mode 100644 index 0000000..2738a53 --- /dev/null +++ b/src/main/java/cn/hutool/core/package-info.java @@ -0,0 +1,7 @@ +/** + * Hutool核心方法及数据结构包 + * + * @author looly + * + */ +package cn.hutool.core; \ No newline at end of file diff --git a/src/main/java/cn/hutool/core/stream/CollectorUtil.java b/src/main/java/cn/hutool/core/stream/CollectorUtil.java new file mode 100644 index 0000000..f474c53 --- /dev/null +++ b/src/main/java/cn/hutool/core/stream/CollectorUtil.java @@ -0,0 +1,314 @@ +package cn.hutool.core.stream; + +import cn.hutool.core.lang.Opt; +import cn.hutool.core.util.StrUtil; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.EnumSet; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.StringJoiner; +import java.util.function.BiConsumer; +import java.util.function.BinaryOperator; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.stream.Collector; +import java.util.stream.Collectors; + +/** + * 可变的汇聚操作{@link Collector} 相关工具封装 + * + * @author looly, VampireAchao + * @since 5.6.7 + */ +public class CollectorUtil { + + /** + * 说明已包含IDENTITY_FINISH特征 为 Characteristics.IDENTITY_FINISH 的缩写 + */ + public static final Set CH_ID + = Collections.unmodifiableSet(EnumSet.of(Collector.Characteristics.IDENTITY_FINISH)); + /** + * 说明不包含IDENTITY_FINISH特征 + */ + public static final Set CH_NOID = Collections.emptySet(); + + /** + * 提供任意对象的Join操作的{@link Collector}实现,对象默认调用toString方法 + * + * @param delimiter 分隔符 + * @param 对象类型 + * @return {@link Collector} + */ + public static Collector joining(CharSequence delimiter) { + return joining(delimiter, Object::toString); + } + + /** + * 提供任意对象的Join操作的{@link Collector}实现 + * + * @param delimiter 分隔符 + * @param toStringFunc 自定义指定对象转换为字符串的方法 + * @param 对象类型 + * @return {@link Collector} + */ + public static Collector joining(CharSequence delimiter, + Function toStringFunc) { + return joining(delimiter, StrUtil.EMPTY, StrUtil.EMPTY, toStringFunc); + } + + /** + * 提供任意对象的Join操作的{@link Collector}实现 + * + * @param delimiter 分隔符 + * @param prefix 前缀 + * @param suffix 后缀 + * @param toStringFunc 自定义指定对象转换为字符串的方法 + * @param 对象类型 + * @return {@link Collector} + */ + public static Collector joining(CharSequence delimiter, + CharSequence prefix, + CharSequence suffix, + Function toStringFunc) { + return new SimpleCollector<>( + () -> new StringJoiner(delimiter, prefix, suffix), + (joiner, ele) -> joiner.add(toStringFunc.apply(ele)), + StringJoiner::merge, + StringJoiner::toString, + Collections.emptySet() + ); + } + + + /** + * 提供对null值友好的groupingBy操作的{@link Collector}实现,可指定map类型 + * + * @param classifier 分组依据 + * @param mapFactory 提供的map + * @param downstream 下游操作 + * @param 实体类型 + * @param 实体中的分组依据对应类型,也是Map中key的类型 + * @param 下游操作对应返回类型,也是Map中value的类型 + * @param 下游操作在进行中间操作时对应类型 + * @param 最后返回结果Map类型 + * @return {@link Collector} + */ + public static > Collector groupingBy(Function classifier, + Supplier mapFactory, + Collector downstream) { + final Supplier downstreamSupplier = downstream.supplier(); + final BiConsumer downstreamAccumulator = downstream.accumulator(); + final BiConsumer, T> accumulator = (m, t) -> { + final K key = Opt.ofNullable(t).map(classifier).orElse(null); + final A container = m.computeIfAbsent(key, k -> downstreamSupplier.get()); + downstreamAccumulator.accept(container, t); + }; + final BinaryOperator> merger = mapMerger(downstream.combiner()); + @SuppressWarnings("unchecked") final Supplier> mangledFactory = (Supplier>) mapFactory; + + if (downstream.characteristics().contains(Collector.Characteristics.IDENTITY_FINISH)) { + return new SimpleCollector<>(mangledFactory, accumulator, merger, CH_ID); + } else { + @SuppressWarnings("unchecked") final Function downstreamFinisher = (Function) downstream.finisher(); + final Function, M> finisher = intermediate -> { + intermediate.replaceAll((k, v) -> downstreamFinisher.apply(v)); + @SuppressWarnings("unchecked") final M castResult = (M) intermediate; + return castResult; + }; + return new SimpleCollector<>(mangledFactory, accumulator, merger, finisher, CH_NOID); + } + } + + /** + * 提供对null值友好的groupingBy操作的{@link Collector}实现 + * + * @param classifier 分组依据 + * @param downstream 下游操作 + * @param 实体类型 + * @param 实体中的分组依据对应类型,也是Map中key的类型 + * @param 下游操作对应返回类型,也是Map中value的类型 + * @param 下游操作在进行中间操作时对应类型 + * @return {@link Collector} + */ + public static + Collector> groupingBy(Function classifier, + Collector downstream) { + return groupingBy(classifier, HashMap::new, downstream); + } + + /** + * 提供对null值友好的groupingBy操作的{@link Collector}实现 + * + * @param classifier 分组依据 + * @param 实体类型 + * @param 实体中的分组依据对应类型,也是Map中key的类型 + * @return {@link Collector} + */ + public static Collector>> + groupingBy(Function classifier) { + return groupingBy(classifier, Collectors.toList()); + } + + /** + * 对null友好的 toMap 操作的 {@link Collector}实现,默认使用HashMap + * + * @param keyMapper 指定map中的key + * @param valueMapper 指定map中的value + * @param mergeFunction 合并前对value进行的操作 + * @param 实体类型 + * @param map中key的类型 + * @param map中value的类型 + * @return 对null友好的 toMap 操作的 {@link Collector}实现 + */ + public static + Collector> toMap(Function keyMapper, + Function valueMapper, + BinaryOperator mergeFunction) { + return toMap(keyMapper, valueMapper, mergeFunction, HashMap::new); + } + + /** + * 对null友好的 toMap 操作的 {@link Collector}实现 + * + * @param keyMapper 指定map中的key + * @param valueMapper 指定map中的value + * @param mergeFunction 合并前对value进行的操作 + * @param mapSupplier 最终需要的map类型 + * @param 实体类型 + * @param map中key的类型 + * @param map中value的类型 + * @param map的类型 + * @return 对null友好的 toMap 操作的 {@link Collector}实现 + */ + public static > + Collector toMap(Function keyMapper, + Function valueMapper, + BinaryOperator mergeFunction, + Supplier mapSupplier) { + BiConsumer accumulator + = (map, element) -> map.put(Opt.ofNullable(element).map(keyMapper).get(), Opt.ofNullable(element).map(valueMapper).get()); + return new SimpleCollector<>(mapSupplier, accumulator, mapMerger(mergeFunction), CH_ID); + } + + /** + * 用户合并map的BinaryOperator,传入合并前需要对value进行的操作 + * + * @param mergeFunction 合并前需要对value进行的操作 + * @param key的类型 + * @param value的类型 + * @param map + * @return 用户合并map的BinaryOperator + */ + public static > BinaryOperator mapMerger(BinaryOperator mergeFunction) { + return (m1, m2) -> { + for (Map.Entry e : m2.entrySet()) { + m1.merge(e.getKey(), e.getValue(), mergeFunction); + } + return m1; + }; + } + + /** + * 聚合这种数据类型:{@code Collection> => Map>} + * 其中key相同的value,会累加到List中 + * + * @param key的类型 + * @param value的类型 + * @return 聚合后的map + * @since 5.8.5 + */ + public static Collector, ?, Map>> reduceListMap() { + return reduceListMap(HashMap::new); + } + + /** + * 聚合这种数据类型:{@code Collection> => Map>} + * 其中key相同的value,会累加到List中 + * + * @param mapSupplier 可自定义map的类型如concurrentHashMap等 + * @param key的类型 + * @param value的类型 + * @param 返回值的类型 + * @return 聚合后的map + * @since 5.8.5 + */ + public static >> Collector, ?, R> reduceListMap(final Supplier mapSupplier) { + return Collectors.reducing(mapSupplier.get(), value -> { + final R result = mapSupplier.get(); + value.forEach((k, v) -> result.computeIfAbsent(k, i -> new ArrayList<>()).add(v)); + return result; + }, (l, r) -> { + r.forEach((k, v) -> l.computeIfAbsent(k, i -> new ArrayList<>()).addAll(v)); + return l; + } + ); + } + + /** + * 提供对null值友好的groupingBy操作的{@link Collector}实现, + * 对集合分组,然后对分组后的值集合进行映射 + * + * @param classifier 分组依据 + * @param valueMapper 值映射方法 + * @param valueCollFactory 值集合的工厂方法 + * @param mapFactory Map集合的工厂方法 + * @param 元素类型 + * @param 键类型 + * @param 值类型 + * @param 值集合类型 + * @param 返回的Map集合类型 + * @return {@link Collector} + */ + public static , M extends Map> Collector groupingBy( + final Function classifier, + final Function valueMapper, + final Supplier valueCollFactory, + final Supplier mapFactory) { + return groupingBy(classifier, mapFactory, Collectors.mapping( + valueMapper, Collectors.toCollection(valueCollFactory) + )); + } + + /** + * 提供对null值友好的groupingBy操作的{@link Collector}实现, + * 对集合分组,然后对分组后的值集合进行映射 + * + * @param classifier 分组依据 + * @param valueMapper 值映射方法 + * @param valueCollFactory 值集合的工厂方法 + * @param 元素类型 + * @param 键类型 + * @param 值类型 + * @param 值集合类型 + * @return {@link Collector} + */ + public static > Collector> groupingBy( + final Function classifier, + final Function valueMapper, + final Supplier valueCollFactory) { + return groupingBy(classifier, valueMapper, valueCollFactory, HashMap::new); + } + + /** + * 提供对null值友好的groupingBy操作的{@link Collector}实现, + * 对集合分组,然后对分组后的值集合进行映射 + * + * @param classifier 分组依据 + * @param valueMapper 值映射方法 + * @param 元素类型 + * @param 键类型 + * @param 值类型 + * @return {@link Collector} + */ + public static Collector>> groupingBy( + final Function classifier, + final Function valueMapper) { + return groupingBy(classifier, valueMapper, ArrayList::new, HashMap::new); + } + +} diff --git a/src/main/java/cn/hutool/core/stream/SimpleCollector.java b/src/main/java/cn/hutool/core/stream/SimpleCollector.java new file mode 100644 index 0000000..811c99e --- /dev/null +++ b/src/main/java/cn/hutool/core/stream/SimpleCollector.java @@ -0,0 +1,109 @@ +package cn.hutool.core.stream; + +import java.util.Set; +import java.util.function.BiConsumer; +import java.util.function.BinaryOperator; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.stream.Collector; + +/** + * 简单{@link Collector}接口实现 + * + * @param 输入数据类型 + * @param 累积结果的容器类型 + * @param 数据结果类型 + * @since 5.6.7 + */ +public class SimpleCollector implements Collector { + /** + * 创建新的结果容器,容器类型为A + */ + private final Supplier supplier; + /** + * 将输入元素合并到结果容器中 + */ + private final BiConsumer accumulator; + /** + * 合并两个结果容器(并行流使用,将多个线程产生的结果容器合并) + */ + private final BinaryOperator combiner; + /** + * 将结果容器转换成最终的表示 + */ + private final Function finisher; + /** + * 特征值枚举,见{@link Characteristics} + *
    + *
  • CONCURRENT: 表示结果容器只有一个(即使是在并行流的情况下)。 + * 只有在并行流且收集器不具备此特性的情况下,combiner()返回的lambda表达式才会执行(中间结果容器只有一个就无需合并)。 + * 设置此特性时意味着多个线程可以对同一个结果容器调用,因此结果容器必须是线程安全的。
  • + *
  • UNORDERED: 表示流中的元素无序
  • + *
  • IDENTITY_FINISH:表示中间结果容器类型与最终结果类型一致。设置此特性时finisher()方法不会被调用
  • + *
+ */ + private final Set characteristics; + + /** + * 构造 + * + * @param supplier 创建新的结果容器函数 + * @param accumulator 将输入元素合并到结果容器中函数 + * @param combiner 合并两个结果容器函数(并行流使用,将多个线程产生的结果容器合并) + * @param finisher 将结果容器转换成最终的表示函数 + * @param characteristics 特征值枚举 + */ + public SimpleCollector(Supplier
supplier, + BiConsumer accumulator, + BinaryOperator combiner, + Function finisher, + Set characteristics) { + this.supplier = supplier; + this.accumulator = accumulator; + this.combiner = combiner; + this.finisher = finisher; + this.characteristics = characteristics; + } + + /** + * 构造 + * + * @param supplier 创建新的结果容器函数 + * @param accumulator 将输入元素合并到结果容器中函数 + * @param combiner 合并两个结果容器函数(并行流使用,将多个线程产生的结果容器合并) + * @param characteristics 特征值枚举 + */ + @SuppressWarnings("unchecked") + public SimpleCollector(Supplier supplier, + BiConsumer accumulator, + BinaryOperator combiner, + Set characteristics) { + this(supplier, accumulator, combiner, i -> (R) i, characteristics); + } + + @Override + public BiConsumer accumulator() { + return accumulator; + } + + @Override + public Supplier supplier() { + return supplier; + } + + @Override + public BinaryOperator combiner() { + return combiner; + } + + @Override + public Function finisher() { + return finisher; + } + + @Override + public Set characteristics() { + return characteristics; + } + +} diff --git a/src/main/java/cn/hutool/core/stream/StreamUtil.java b/src/main/java/cn/hutool/core/stream/StreamUtil.java new file mode 100644 index 0000000..475a542 --- /dev/null +++ b/src/main/java/cn/hutool/core/stream/StreamUtil.java @@ -0,0 +1,170 @@ +package cn.hutool.core.stream; + +import cn.hutool.core.io.IORuntimeException; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.CharsetUtil; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.Charset; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Collection; +import java.util.Iterator; +import java.util.Spliterators; +import java.util.function.Function; +import java.util.function.UnaryOperator; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; + +/** + * {@link Stream} 工具类 + * + * @author looly + * @since 5.6.7 + */ +public class StreamUtil { + + @SafeVarargs + public static Stream of(T... array) { + Assert.notNull(array, "Array must be not null!"); + return Stream.of(array); + } + + /** + * {@link Iterable}转换为{@link Stream},默认非并行 + * + * @param iterable 集合 + * @param 集合元素类型 + * @return {@link Stream} + */ + public static Stream of(Iterable iterable) { + return of(iterable, false); + } + + /** + * {@link Iterable}转换为{@link Stream} + * + * @param iterable 集合 + * @param parallel 是否并行 + * @param 集合元素类型 + * @return {@link Stream} + */ + public static Stream of(Iterable iterable, boolean parallel) { + Assert.notNull(iterable, "Iterable must be not null!"); + + return iterable instanceof Collection ? + parallel ? ((Collection) iterable).parallelStream() : ((Collection) iterable).stream() : + StreamSupport.stream(iterable.spliterator(), parallel); + } + + /** + * {@link Iterator} 转换为 {@link Stream} + * @param iterator 迭代器 + * @param 集合元素类型 + * @return {@link Stream} + * @throws IllegalArgumentException 如果iterator为null,抛出该异常 + */ + public static Stream of(Iterator iterator) { + return of(iterator, false); + } + + /** + * {@link Iterator} 转换为 {@link Stream} + * @param iterator 迭代器 + * @param parallel 是否并行 + * @param 集合元素类型 + * @return {@link Stream} + * @throws IllegalArgumentException 如果iterator为null,抛出该异常 + */ + public static Stream of(Iterator iterator, boolean parallel) { + Assert.notNull(iterator, "iterator must not be null!"); + return StreamSupport.stream(Spliterators.spliteratorUnknownSize(iterator, 0), parallel); + } + + /** + * 按行读取文件为{@link Stream} + * + * @param file 文件 + * @return {@link Stream} + */ + public static Stream of(File file) { + return of(file, CharsetUtil.CHARSET_UTF_8); + } + + /** + * 按行读取文件为{@link Stream} + * + * @param path 路径 + * @return {@link Stream} + */ + public static Stream of(Path path) { + return of(path, CharsetUtil.CHARSET_UTF_8); + } + + /** + * 按行读取文件为{@link Stream} + * + * @param file 文件 + * @param charset 编码 + * @return {@link Stream} + */ + public static Stream of(File file, Charset charset) { + Assert.notNull(file, "File must be not null!"); + return of(file.toPath(), charset); + } + + /** + * 按行读取文件为{@link Stream} + * + * @param path 路径 + * @param charset 编码 + * @return {@link Stream} + */ + public static Stream of(Path path, Charset charset) { + try { + return Files.lines(path, charset); + } catch (IOException e) { + throw new IORuntimeException(e); + } + } + + /** + * 通过函数创建Stream + * + * @param seed 初始值 + * @param elementCreator 递进函数,每次调用此函数获取下一个值 + * @param limit 限制个数 + * @param 创建元素类型 + * @return {@link Stream} + */ + public static Stream of(T seed, UnaryOperator elementCreator, int limit) { + return Stream.iterate(seed, elementCreator).limit(limit); + } + + /** + * 将Stream中所有元素以指定分隔符,合并为一个字符串,对象默认调用toString方法 + * + * @param stream {@link Stream} + * @param delimiter 分隔符 + * @param 元素类型 + * @return 字符串 + */ + public static String join(Stream stream, CharSequence delimiter) { + return stream.collect(CollectorUtil.joining(delimiter)); + } + + /** + * 将Stream中所有元素以指定分隔符,合并为一个字符串 + * + * @param stream {@link Stream} + * @param delimiter 分隔符 + * @param toStringFunc 元素转换为字符串的函数 + * @param 元素类型 + * @return 字符串 + */ + public static String join(Stream stream, CharSequence delimiter, + Function toStringFunc) { + return stream.collect(CollectorUtil.joining(delimiter, toStringFunc)); + } +} diff --git a/src/main/java/cn/hutool/core/stream/package-info.java b/src/main/java/cn/hutool/core/stream/package-info.java new file mode 100644 index 0000000..155cf24 --- /dev/null +++ b/src/main/java/cn/hutool/core/stream/package-info.java @@ -0,0 +1,7 @@ +/** + * Java8的stream相关封装 + * + * @author looly + * + */ +package cn.hutool.core.stream; diff --git a/src/main/java/cn/hutool/core/text/ASCIIStrCache.java b/src/main/java/cn/hutool/core/text/ASCIIStrCache.java new file mode 100644 index 0000000..fd2b0df --- /dev/null +++ b/src/main/java/cn/hutool/core/text/ASCIIStrCache.java @@ -0,0 +1,30 @@ +package cn.hutool.core.text; + +/** + * ASCII字符对应的字符串缓存 + * + * @author looly + * @since 4.0.1 + * + */ +public class ASCIIStrCache { + + private static final int ASCII_LENGTH = 128; + private static final String[] CACHE = new String[ASCII_LENGTH]; + static { + for (char c = 0; c < ASCII_LENGTH; c++) { + CACHE[c] = String.valueOf(c); + } + } + + /** + * 字符转为字符串
+ * 如果为ASCII字符,使用缓存 + * + * @param c 字符 + * @return 字符串 + */ + public static String toString(char c) { + return c < ASCII_LENGTH ? CACHE[c] : String.valueOf(c); + } +} diff --git a/src/main/java/cn/hutool/core/text/AntPathMatcher.java b/src/main/java/cn/hutool/core/text/AntPathMatcher.java new file mode 100644 index 0000000..9eeaa48 --- /dev/null +++ b/src/main/java/cn/hutool/core/text/AntPathMatcher.java @@ -0,0 +1,945 @@ +package cn.hutool.core.text; + + +import cn.hutool.core.map.SafeConcurrentHashMap; +import cn.hutool.core.util.StrUtil; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Ant风格的路径匹配器。
+ * 来自Spring-core和Ant + * + *

匹配URL的规则如下:
+ *

    + *
  • {@code ?} 匹配单个字符
  • + *
  • {@code *} 匹配0个或多个字符
  • + *
  • {@code **} 0个或多个路径中的目录节点
  • + *
  • {@code {hutool:[a-z]+}} 匹配以"hutool"命名的正则 {@code [a-z]+}
  • + *
+ * + *

例子:

+ *
    + *
  • {@code com/t?st.jsp} — 匹配 {@code com/test.jsp} 或 {@code com/tast.jsp} 或 {@code com/txst.jsp}
  • + *
  • {@code com/*.jsp} — 匹配{@code com}目录下全部 {@code .jsp}文件
  • + *
  • {@code com/**/test.jsp} — 匹配{@code com}目录下全部 {@code test.jsp}文件
  • + *
  • {@code cn/hutool/**/*.jsp} — 匹配{@code cn/hutool}路径下全部{@code .jsp} 文件
  • + *
  • {@code org/**/servlet/bla.jsp} — 匹配{@code cn/hutool/servlet/bla.jsp} 或{@code cn/hutool/testing/servlet/bla.jsp} 或 {@code org/servlet/bla.jsp}
  • + *
  • {@code com/{filename:\\w+}.jsp} 匹配 {@code com/test.jsp} 并将 {@code test} 关联到 {@code filename} 变量
  • + *
+ * + *

注意: 表达式和路径必须都为绝对路径或都为相对路径。 + * + * @author Alef Arendsen, Juergen Hoeller, Rob Harrop, Arjen Poutsma, Rossen Stoyanchev, Sam Brannen, Vladislav Kisel + * @since 5.7.22 + */ +public class AntPathMatcher { + + /** + * Default path separator: "/". + */ + public static final String DEFAULT_PATH_SEPARATOR = StrUtil.SLASH; + + private static final int CACHE_TURNOFF_THRESHOLD = 65536; + + private static final Pattern VARIABLE_PATTERN = Pattern.compile("\\{[^/]+?}"); + + private static final char[] WILDCARD_CHARS = {'*', '?', '{'}; + + private String pathSeparator; + + private PathSeparatorPatternCache pathSeparatorPatternCache; + + private boolean caseSensitive = true; + + private boolean trimTokens = false; + + private volatile Boolean cachePatterns; + + private final Map tokenizedPatternCache = new SafeConcurrentHashMap<>(256); + + private final Map stringMatcherCache = new SafeConcurrentHashMap<>(256); + + + /** + * 使用 {@link #DEFAULT_PATH_SEPARATOR} 作为分隔符构造 + */ + public AntPathMatcher() { + this(DEFAULT_PATH_SEPARATOR); + } + + /** + * 使用自定义的分隔符构造 + * + * @param pathSeparator the path separator to use, must not be {@code null}. + * @since 4.1 + */ + public AntPathMatcher(String pathSeparator) { + if (null == pathSeparator) { + pathSeparator = DEFAULT_PATH_SEPARATOR; + } + setPathSeparator(pathSeparator); + } + + + /** + * 设置路径分隔符 + * + * @param pathSeparator 分隔符,{@code null}表示使用默认分隔符{@link #DEFAULT_PATH_SEPARATOR} + * @return this + */ + public AntPathMatcher setPathSeparator(String pathSeparator) { + if (null == pathSeparator) { + pathSeparator = DEFAULT_PATH_SEPARATOR; + } + this.pathSeparator = pathSeparator; + this.pathSeparatorPatternCache = new PathSeparatorPatternCache(this.pathSeparator); + return this; + } + + /** + * 设置是否大小写敏感,默认为{@code true} + * + * @param caseSensitive 是否大小写敏感 + * @return this + */ + public AntPathMatcher setCaseSensitive(boolean caseSensitive) { + this.caseSensitive = caseSensitive; + return this; + } + + /** + * 设置是否去除路径节点两边的空白符,默认为{@code false} + * + * @param trimTokens 是否去除路径节点两边的空白符 + * @return this + */ + public AntPathMatcher setTrimTokens(boolean trimTokens) { + this.trimTokens = trimTokens; + return this; + } + + /** + * Specify whether to cache parsed pattern metadata for patterns passed + * into this matcher's {@link #match} method. A value of {@code true} + * activates an unlimited pattern cache; a value of {@code false} turns + * the pattern cache off completely. + *

Default is for the cache to be on, but with the variant to automatically + * turn it off when encountering too many patterns to cache at runtime + * (the threshold is 65536), assuming that arbitrary permutations of patterns + * are coming in, with little chance for encountering a recurring pattern. + * + * @param cachePatterns 是否缓存表达式 + * @return this + * @see #getStringMatcher(String) + */ + public AntPathMatcher setCachePatterns(boolean cachePatterns) { + this.cachePatterns = cachePatterns; + return this; + } + + /** + * 判断给定路径是否是表达式 + * + * @param path 路径 + * @return 是否为表达式 + */ + public boolean isPattern(String path) { + if (path == null) { + return false; + } + boolean uriVar = false; + final int length = path.length(); + char c; + for (int i = 0; i < length; i++) { + c = path.charAt(i); + // 含有通配符 + if (c == '*' || c == '?') { + return true; + } + if (c == CharPool.DELIM_START) { + uriVar = true; + continue; + } + if (c == CharPool.DELIM_END && uriVar) { + return true; + } + } + return false; + } + + /** + * 给定路径是否匹配表达式 + * + * @param pattern 表达式 + * @param path 路径 + * @return 是否匹配 + */ + public boolean match(String pattern, String path) { + return doMatch(pattern, path, true, null); + } + + /** + * 前置部分匹配 + * + * @param pattern 表达式 + * @param path 路径 + * @return 是否匹配 + */ + public boolean matchStart(String pattern, String path) { + return doMatch(pattern, path, false, null); + } + + /** + * 执行匹配,判断给定的{@code path}是否匹配{@code pattern} + * + * @param pattern 表达式 + * @param path 路径 + * @param fullMatch 是否全匹配。{@code true} 表示全路径匹配,{@code false}表示只匹配开始 + * @param uriTemplateVariables 变量映射 + * @return {@code true} 表示提供的 {@code path} 匹配, {@code false} 表示不匹配 + */ + protected boolean doMatch(String pattern, String path, boolean fullMatch, Map uriTemplateVariables) { + if (path == null || path.startsWith(this.pathSeparator) != pattern.startsWith(this.pathSeparator)) { + return false; + } + + final String[] pattDirs = tokenizePattern(pattern); + if (fullMatch && this.caseSensitive && !isPotentialMatch(path, pattDirs)) { + return false; + } + + final String[] pathDirs = tokenizePath(path); + int pattIdxStart = 0; + int pattIdxEnd = pattDirs.length - 1; + int pathIdxStart = 0; + int pathIdxEnd = pathDirs.length - 1; + + // Match all elements up to the first ** + while (pattIdxStart <= pattIdxEnd && pathIdxStart <= pathIdxEnd) { + String pattDir = pattDirs[pattIdxStart]; + if ("**".equals(pattDir)) { + break; + } + if (notMatchStrings(pattDir, pathDirs[pathIdxStart], uriTemplateVariables)) { + return false; + } + pattIdxStart++; + pathIdxStart++; + } + + if (pathIdxStart > pathIdxEnd) { + // Path is exhausted, only match if rest of pattern is * or **'s + if (pattIdxStart > pattIdxEnd) { + return (pattern.endsWith(this.pathSeparator) == path.endsWith(this.pathSeparator)); + } + if (!fullMatch) { + return true; + } + if (pattIdxStart == pattIdxEnd && pattDirs[pattIdxStart].equals("*") && path.endsWith(this.pathSeparator)) { + return true; + } + for (int i = pattIdxStart; i <= pattIdxEnd; i++) { + if (!pattDirs[i].equals("**")) { + return false; + } + } + return true; + } else if (pattIdxStart > pattIdxEnd) { + // String not exhausted, but pattern is. Failure. + return false; + } else if (!fullMatch && "**".equals(pattDirs[pattIdxStart])) { + // Path start definitely matches due to "**" part in pattern. + return true; + } + + // up to last '**' + while (pattIdxStart <= pattIdxEnd && pathIdxStart <= pathIdxEnd) { + String pattDir = pattDirs[pattIdxEnd]; + if (pattDir.equals("**")) { + break; + } + if (notMatchStrings(pattDir, pathDirs[pathIdxEnd], uriTemplateVariables)) { + return false; + } + pattIdxEnd--; + pathIdxEnd--; + } + if (pathIdxStart > pathIdxEnd) { + // String is exhausted + for (int i = pattIdxStart; i <= pattIdxEnd; i++) { + if (!pattDirs[i].equals("**")) { + return false; + } + } + return true; + } + + while (pattIdxStart != pattIdxEnd && pathIdxStart <= pathIdxEnd) { + int patIdxTmp = -1; + for (int i = pattIdxStart + 1; i <= pattIdxEnd; i++) { + if (pattDirs[i].equals("**")) { + patIdxTmp = i; + break; + } + } + if (patIdxTmp == pattIdxStart + 1) { + // '**/**' situation, so skip one + pattIdxStart++; + continue; + } + // Find the pattern between padIdxStart & padIdxTmp in str between + // strIdxStart & strIdxEnd + int patLength = (patIdxTmp - pattIdxStart - 1); + int strLength = (pathIdxEnd - pathIdxStart + 1); + int foundIdx = -1; + + strLoop: + for (int i = 0; i <= strLength - patLength; i++) { + for (int j = 0; j < patLength; j++) { + String subPat = pattDirs[pattIdxStart + j + 1]; + String subStr = pathDirs[pathIdxStart + i + j]; + if (notMatchStrings(subPat, subStr, uriTemplateVariables)) { + continue strLoop; + } + } + foundIdx = pathIdxStart + i; + break; + } + + if (foundIdx == -1) { + return false; + } + + pattIdxStart = patIdxTmp; + pathIdxStart = foundIdx + patLength; + } + + for (int i = pattIdxStart; i <= pattIdxEnd; i++) { + if (!pattDirs[i].equals("**")) { + return false; + } + } + + return true; + } + + private boolean isPotentialMatch(String path, String[] pattDirs) { + if (!this.trimTokens) { + int pos = 0; + for (String pattDir : pattDirs) { + int skipped = skipSeparator(path, pos, this.pathSeparator); + pos += skipped; + skipped = skipSegment(path, pos, pattDir); + if (skipped < pattDir.length()) { + return (skipped > 0 || (pattDir.length() > 0 && isWildcardChar(pattDir.charAt(0)))); + } + pos += skipped; + } + } + return true; + } + + private int skipSegment(String path, int pos, String prefix) { + int skipped = 0; + for (int i = 0; i < prefix.length(); i++) { + char c = prefix.charAt(i); + if (isWildcardChar(c)) { + return skipped; + } + int currPos = pos + skipped; + if (currPos >= path.length()) { + return 0; + } + if (c == path.charAt(currPos)) { + skipped++; + } + } + return skipped; + } + + private int skipSeparator(String path, int pos, String separator) { + int skipped = 0; + while (path.startsWith(separator, pos + skipped)) { + skipped += separator.length(); + } + return skipped; + } + + private boolean isWildcardChar(char c) { + for (char candidate : WILDCARD_CHARS) { + if (c == candidate) { + return true; + } + } + return false; + } + + /** + * Tokenize the given path pattern into parts, based on this matcher's settings. + *

Performs caching based on {@link #setCachePatterns}, delegating to + * {@link #tokenizePath(String)} for the actual tokenization algorithm. + * + * @param pattern the pattern to tokenize + * @return the tokenized pattern parts + */ + protected String[] tokenizePattern(String pattern) { + String[] tokenized = null; + Boolean cachePatterns = this.cachePatterns; + if (cachePatterns == null || cachePatterns) { + tokenized = this.tokenizedPatternCache.get(pattern); + } + if (tokenized == null) { + tokenized = tokenizePath(pattern); + if (cachePatterns == null && this.tokenizedPatternCache.size() >= CACHE_TURNOFF_THRESHOLD) { + // Try to adapt to the runtime situation that we're encountering: + // There are obviously too many different patterns coming in here... + // So let's turn off the cache since the patterns are unlikely to be reoccurring. + deactivatePatternCache(); + return tokenized; + } + if (cachePatterns == null || cachePatterns) { + this.tokenizedPatternCache.put(pattern, tokenized); + } + } + return tokenized; + } + + private void deactivatePatternCache() { + this.cachePatterns = false; + this.tokenizedPatternCache.clear(); + this.stringMatcherCache.clear(); + } + + /** + * Tokenize the given path into parts, based on this matcher's settings. + * + * @param path the path to tokenize + * @return the tokenized path parts + */ + protected String[] tokenizePath(String path) { + return StrSplitter.splitToArray(path, this.pathSeparator, 0, this.trimTokens, true); + } + + /** + * Test whether or not a string matches against a pattern. + * + * @param pattern the pattern to match against (never {@code null}) + * @param str the String which must be matched against the pattern (never {@code null}) + * @return {@code true} if the string matches against the pattern, or {@code false} otherwise + */ + private boolean notMatchStrings(String pattern, String str, Map uriTemplateVariables) { + return !getStringMatcher(pattern).matchStrings(str, uriTemplateVariables); + } + + /** + * Build or retrieve an {@link AntPathStringMatcher} for the given pattern. + *

The default implementation checks this AntPathMatcher's internal cache + * (see {@link #setCachePatterns}), creating a new AntPathStringMatcher instance + * if no cached copy is found. + *

When encountering too many patterns to cache at runtime (the threshold is 65536), + * it turns the default cache off, assuming that arbitrary permutations of patterns + * are coming in, with little chance for encountering a recurring pattern. + *

This method may be overridden to implement a custom cache strategy. + * + * @param pattern the pattern to match against (never {@code null}) + * @return a corresponding AntPathStringMatcher (never {@code null}) + * @see #setCachePatterns + */ + protected AntPathStringMatcher getStringMatcher(String pattern) { + AntPathStringMatcher matcher = null; + Boolean cachePatterns = this.cachePatterns; + if (cachePatterns == null || cachePatterns) { + matcher = this.stringMatcherCache.get(pattern); + } + if (matcher == null) { + matcher = new AntPathStringMatcher(pattern, this.caseSensitive); + if (cachePatterns == null && this.stringMatcherCache.size() >= CACHE_TURNOFF_THRESHOLD) { + // Try to adapt to the runtime situation that we're encountering: + // There are obviously too many different patterns coming in here... + // So let's turn off the cache since the patterns are unlikely to be reoccurring. + deactivatePatternCache(); + return matcher; + } + if (cachePatterns == null || cachePatterns) { + this.stringMatcherCache.put(pattern, matcher); + } + } + return matcher; + } + + /** + * Given a pattern and a full path, determine the pattern-mapped part.

For example:

    + *
  • '{@code /docs/cvs/commit.html}' and '{@code /docs/cvs/commit.html} → ''
  • + *
  • '{@code /docs/*}' and '{@code /docs/cvs/commit} → '{@code cvs/commit}'
  • + *
  • '{@code /docs/cvs/*.html}' and '{@code /docs/cvs/commit.html} → '{@code commit.html}'
  • + *
  • '{@code /docs/**}' and '{@code /docs/cvs/commit} → '{@code cvs/commit}'
  • + *
  • '{@code /docs/**\/*.html}' and '{@code /docs/cvs/commit.html} → '{@code cvs/commit.html}'
  • + *
  • '{@code /*.html}' and '{@code /docs/cvs/commit.html} → '{@code docs/cvs/commit.html}'
  • + *
  • '{@code *.html}' and '{@code /docs/cvs/commit.html} → '{@code /docs/cvs/commit.html}'
  • + *
  • '{@code *}' and '{@code /docs/cvs/commit.html} → '{@code /docs/cvs/commit.html}'
+ *

Assumes that {@link #match} returns {@code true} for '{@code pattern}' and '{@code path}', but + * does not enforce this. + * + * @param pattern 表达式 + * @param path 路径 + * @return 表达式匹配到的部分 + */ + public String extractPathWithinPattern(String pattern, String path) { + String[] patternParts = tokenizePath(pattern); + String[] pathParts = tokenizePath(path); + StringBuilder builder = new StringBuilder(); + boolean pathStarted = false; + + for (int segment = 0; segment < patternParts.length; segment++) { + String patternPart = patternParts[segment]; + if (patternPart.indexOf('*') > -1 || patternPart.indexOf('?') > -1) { + for (; segment < pathParts.length; segment++) { + if (pathStarted || (segment == 0 && !pattern.startsWith(this.pathSeparator))) { + builder.append(this.pathSeparator); + } + builder.append(pathParts[segment]); + pathStarted = true; + } + } + } + + return builder.toString(); + } + + public Map extractUriTemplateVariables(String pattern, String path) { + Map variables = new LinkedHashMap<>(); + boolean result = doMatch(pattern, path, true, variables); + if (!result) { + throw new IllegalStateException("Pattern \"" + pattern + "\" is not a match for \"" + path + "\""); + } + return variables; + } + + /** + * Combine two patterns into a new pattern. + *

This implementation simply concatenates the two patterns, unless + * the first pattern contains a file extension match (e.g., {@code *.html}). + * In that case, the second pattern will be merged into the first. Otherwise, + * an {@code IllegalArgumentException} will be thrown. + *

Examples

+ * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
Pattern 1Pattern 2Result
{@code null}{@code null} 
/hotels{@code null}/hotels
{@code null}/hotels/hotels
/hotels/bookings/hotels/bookings
/hotelsbookings/hotels/bookings
/hotels/*/bookings/hotels/bookings
/hotels/**/bookings/hotels/**/bookings
/hotels{hotel}/hotels/{hotel}
/hotels/*{hotel}/hotels/{hotel}
/hotels/**{hotel}/hotels/**/{hotel}
/*.html/hotels.html/hotels.html
/*.html/hotels/hotels.html
/*.html/*.txt{@code IllegalArgumentException}
+ * + * @param pattern1 the first pattern + * @param pattern2 the second pattern + * @return the combination of the two patterns + * @throws IllegalArgumentException if the two patterns cannot be combined + */ + public String combine(String pattern1, String pattern2) { + if (StrUtil.isEmpty(pattern1) && StrUtil.isEmpty(pattern2)) { + return StrUtil.EMPTY; + } + if (StrUtil.isEmpty(pattern1)) { + return pattern2; + } + if (StrUtil.isEmpty(pattern2)) { + return pattern1; + } + + boolean pattern1ContainsUriVar = (pattern1.indexOf('{') != -1); + if (!pattern1.equals(pattern2) && !pattern1ContainsUriVar && match(pattern1, pattern2)) { + // /* + /hotel -> /hotel ; "/*.*" + "/*.html" -> /*.html + // However /user + /user -> /usr/user ; /{foo} + /bar -> /{foo}/bar + return pattern2; + } + + // /hotels/* + /booking -> /hotels/booking + // /hotels/* + booking -> /hotels/booking + if (pattern1.endsWith(this.pathSeparatorPatternCache.getEndsOnWildCard())) { + return concat(pattern1.substring(0, pattern1.length() - 2), pattern2); + } + + // /hotels/** + /booking -> /hotels/**/booking + // /hotels/** + booking -> /hotels/**/booking + if (pattern1.endsWith(this.pathSeparatorPatternCache.getEndsOnDoubleWildCard())) { + return concat(pattern1, pattern2); + } + + int starDotPos1 = pattern1.indexOf("*."); + if (pattern1ContainsUriVar || starDotPos1 == -1 || this.pathSeparator.equals(".")) { + // simply concatenate the two patterns + return concat(pattern1, pattern2); + } + + String ext1 = pattern1.substring(starDotPos1 + 1); + int dotPos2 = pattern2.indexOf('.'); + String file2 = (dotPos2 == -1 ? pattern2 : pattern2.substring(0, dotPos2)); + String ext2 = (dotPos2 == -1 ? "" : pattern2.substring(dotPos2)); + boolean ext1All = (ext1.equals(".*") || ext1.isEmpty()); + boolean ext2All = (ext2.equals(".*") || ext2.isEmpty()); + if (!ext1All && !ext2All) { + throw new IllegalArgumentException("Cannot combine patterns: " + pattern1 + " vs " + pattern2); + } + String ext = (ext1All ? ext2 : ext1); + return file2 + ext; + } + + private String concat(String path1, String path2) { + boolean path1EndsWithSeparator = path1.endsWith(this.pathSeparator); + boolean path2StartsWithSeparator = path2.startsWith(this.pathSeparator); + + if (path1EndsWithSeparator && path2StartsWithSeparator) { + return path1 + path2.substring(1); + } else if (path1EndsWithSeparator || path2StartsWithSeparator) { + return path1 + path2; + } else { + return path1 + this.pathSeparator + path2; + } + } + + /** + * Given a full path, returns a {@link Comparator} suitable for sorting patterns in order of + * explicitness. + *

This {@code Comparator} will {@linkplain List#sort(Comparator) sort} + * a list so that more specific patterns (without URI templates or wild cards) come before + * generic patterns. So given a list with the following patterns, the returned comparator + * will sort this list so that the order will be as indicated. + *

    + *
  1. {@code /hotels/new}
  2. + *
  3. {@code /hotels/{hotel}}
  4. + *
  5. {@code /hotels/*}
  6. + *
+ *

The full path given as parameter is used to test for exact matches. So when the given path + * is {@code /hotels/2}, the pattern {@code /hotels/2} will be sorted before {@code /hotels/1}. + * + * @param path the full path to use for comparison + * @return a comparator capable of sorting patterns in order of explicitness + */ + public Comparator getPatternComparator(String path) { + return new AntPatternComparator(path); + } + + + /** + * Tests whether or not a string matches against a pattern via a {@link Pattern}. + *

The pattern may contain special characters: '*' means zero or more characters; '?' means one and + * only one character; '{' and '}' indicate a URI template pattern. For example /users/{user}. + */ + protected static class AntPathStringMatcher { + + private static final Pattern GLOB_PATTERN = Pattern.compile("\\?|\\*|\\{((?:\\{[^/]+?}|[^/{}]|\\\\[{}])+?)}"); + + private static final String DEFAULT_VARIABLE_PATTERN = "((?s).*)"; + + private final String rawPattern; + + private final boolean caseSensitive; + + private final boolean exactMatch; + + private final Pattern pattern; + + private final List variableNames = new ArrayList<>(); + + public AntPathStringMatcher(String pattern, boolean caseSensitive) { + this.rawPattern = pattern; + this.caseSensitive = caseSensitive; + StringBuilder patternBuilder = new StringBuilder(); + Matcher matcher = GLOB_PATTERN.matcher(pattern); + int end = 0; + while (matcher.find()) { + patternBuilder.append(quote(pattern, end, matcher.start())); + String match = matcher.group(); + if ("?".equals(match)) { + patternBuilder.append('.'); + } else if ("*".equals(match)) { + patternBuilder.append(".*"); + } else if (match.startsWith("{") && match.endsWith("}")) { + int colonIdx = match.indexOf(':'); + if (colonIdx == -1) { + patternBuilder.append(DEFAULT_VARIABLE_PATTERN); + this.variableNames.add(matcher.group(1)); + } else { + String variablePattern = match.substring(colonIdx + 1, match.length() - 1); + patternBuilder.append('('); + patternBuilder.append(variablePattern); + patternBuilder.append(')'); + String variableName = match.substring(1, colonIdx); + this.variableNames.add(variableName); + } + } + end = matcher.end(); + } + // No glob pattern was found, this is an exact String match + if (end == 0) { + this.exactMatch = true; + this.pattern = null; + } else { + this.exactMatch = false; + patternBuilder.append(quote(pattern, end, pattern.length())); + this.pattern = (this.caseSensitive ? Pattern.compile(patternBuilder.toString()) : + Pattern.compile(patternBuilder.toString(), Pattern.CASE_INSENSITIVE)); + } + } + + private String quote(String s, int start, int end) { + if (start == end) { + return ""; + } + return Pattern.quote(s.substring(start, end)); + } + + /** + * Main entry point. + * + * @param str Str + * @param uriTemplateVariables uri template vars + * @return {@code true} if the string matches against the pattern, or {@code false} otherwise. + */ + public boolean matchStrings(String str, Map uriTemplateVariables) { + if (this.exactMatch) { + return this.caseSensitive ? this.rawPattern.equals(str) : this.rawPattern.equalsIgnoreCase(str); + } else if (this.pattern != null) { + Matcher matcher = this.pattern.matcher(str); + if (matcher.matches()) { + if (uriTemplateVariables != null) { + if (this.variableNames.size() != matcher.groupCount()) { + throw new IllegalArgumentException("The number of capturing groups in the pattern segment " + + this.pattern + " does not match the number of URI template variables it defines, " + + "which can occur if capturing groups are used in a URI template regex. " + + "Use non-capturing groups instead."); + } + for (int i = 1; i <= matcher.groupCount(); i++) { + String name = this.variableNames.get(i - 1); + if (name.startsWith("*")) { + throw new IllegalArgumentException("Capturing patterns (" + name + ") are not " + + "supported by the AntPathMatcher. Use the PathPatternParser instead."); + } + String value = matcher.group(i); + uriTemplateVariables.put(name, value); + } + } + return true; + } + } + return false; + } + + } + + + /** + * The default {@link Comparator} implementation returned by + * {@link #getPatternComparator(String)}. + *

In order, the most "generic" pattern is determined by the following: + *

    + *
  • if it's null or a capture all pattern (i.e. it is equal to "/**")
  • + *
  • if the other pattern is an actual match
  • + *
  • if it's a catch-all pattern (i.e. it ends with "**"
  • + *
  • if it's got more "*" than the other pattern
  • + *
  • if it's got more "{foo}" than the other pattern
  • + *
  • if it's shorter than the other pattern
  • + *
+ */ + protected static class AntPatternComparator implements Comparator { + + private final String path; + + public AntPatternComparator(String path) { + this.path = path; + } + + /** + * Compare two patterns to determine which should match first, i.e. which + * is the most specific regarding the current path. + * + * @param pattern1 表达式1 + * @param pattern2 表达式2 + * @return a negative integer, zero, or a positive integer as pattern1 is + * more specific, equally specific, or less specific than pattern2. + */ + @Override + public int compare(String pattern1, String pattern2) { + PatternInfo info1 = new PatternInfo(pattern1); + PatternInfo info2 = new PatternInfo(pattern2); + + if (info1.isLeastSpecific() && info2.isLeastSpecific()) { + return 0; + } else if (info1.isLeastSpecific()) { + return 1; + } else if (info2.isLeastSpecific()) { + return -1; + } + + boolean pattern1EqualsPath = pattern1.equals(this.path); + boolean pattern2EqualsPath = pattern2.equals(this.path); + if (pattern1EqualsPath && pattern2EqualsPath) { + return 0; + } else if (pattern1EqualsPath) { + return -1; + } else if (pattern2EqualsPath) { + return 1; + } + + if (info1.isPrefixPattern() && info2.isPrefixPattern()) { + return info2.getLength() - info1.getLength(); + } else if (info1.isPrefixPattern() && info2.getDoubleWildcards() == 0) { + return 1; + } else if (info2.isPrefixPattern() && info1.getDoubleWildcards() == 0) { + return -1; + } + + if (info1.getTotalCount() != info2.getTotalCount()) { + return info1.getTotalCount() - info2.getTotalCount(); + } + + if (info1.getLength() != info2.getLength()) { + return info2.getLength() - info1.getLength(); + } + + if (info1.getSingleWildcards() < info2.getSingleWildcards()) { + return -1; + } else if (info2.getSingleWildcards() < info1.getSingleWildcards()) { + return 1; + } + + if (info1.getUriVars() < info2.getUriVars()) { + return -1; + } else if (info2.getUriVars() < info1.getUriVars()) { + return 1; + } + + return 0; + } + + + /** + * Value class that holds information about the pattern, e.g. number of + * occurrences of "*", "**", and "{" pattern elements. + */ + private static class PatternInfo { + + private final String pattern; + private int uriVars; + private int singleWildcards; + private int doubleWildcards; + private boolean catchAllPattern; + private boolean prefixPattern; + private Integer length; + + public PatternInfo(String pattern) { + this.pattern = pattern; + if (this.pattern != null) { + initCounters(); + this.catchAllPattern = this.pattern.equals("/**"); + this.prefixPattern = !this.catchAllPattern && this.pattern.endsWith("/**"); + } + if (this.uriVars == 0) { + this.length = (this.pattern != null ? this.pattern.length() : 0); + } + } + + protected void initCounters() { + int pos = 0; + if (this.pattern != null) { + while (pos < this.pattern.length()) { + if (this.pattern.charAt(pos) == '{') { + this.uriVars++; + pos++; + } else if (this.pattern.charAt(pos) == '*') { + if (pos + 1 < this.pattern.length() && this.pattern.charAt(pos + 1) == '*') { + this.doubleWildcards++; + pos += 2; + } else if (pos > 0 && !this.pattern.substring(pos - 1).equals(".*")) { + this.singleWildcards++; + pos++; + } else { + pos++; + } + } else { + pos++; + } + } + } + } + + public int getUriVars() { + return this.uriVars; + } + + public int getSingleWildcards() { + return this.singleWildcards; + } + + public int getDoubleWildcards() { + return this.doubleWildcards; + } + + public boolean isLeastSpecific() { + return (this.pattern == null || this.catchAllPattern); + } + + public boolean isPrefixPattern() { + return this.prefixPattern; + } + + public int getTotalCount() { + return this.uriVars + this.singleWildcards + (2 * this.doubleWildcards); + } + + /** + * Returns the length of the given pattern, where template variables are considered to be 1 long. + * + * @return 长度 + */ + public int getLength() { + if (this.length == null) { + this.length = (this.pattern != null ? + VARIABLE_PATTERN.matcher(this.pattern).replaceAll("#").length() : 0); + } + return this.length; + } + } + } + + + /** + * A simple cache for patterns that depend on the configured path separator. + */ + private static class PathSeparatorPatternCache { + + private final String endsOnWildCard; + + private final String endsOnDoubleWildCard; + + public PathSeparatorPatternCache(String pathSeparator) { + this.endsOnWildCard = pathSeparator + "*"; + this.endsOnDoubleWildCard = pathSeparator + "**"; + } + + public String getEndsOnWildCard() { + return this.endsOnWildCard; + } + + public String getEndsOnDoubleWildCard() { + return this.endsOnDoubleWildCard; + } + } + +} diff --git a/src/main/java/cn/hutool/core/text/CharPool.java b/src/main/java/cn/hutool/core/text/CharPool.java new file mode 100644 index 0000000..228f022 --- /dev/null +++ b/src/main/java/cn/hutool/core/text/CharPool.java @@ -0,0 +1,86 @@ +package cn.hutool.core.text; + +/** + * 常用字符常量 + * @see StrPool + * @author looly + * @since 5.6.3 + */ +public interface CharPool { + /** + * 字符常量:空格符 {@code ' '} + */ + char SPACE = ' '; + /** + * 字符常量:制表符 {@code '\t'} + */ + char TAB = ' '; + /** + * 字符常量:点 {@code '.'} + */ + char DOT = '.'; + /** + * 字符常量:斜杠 {@code '/'} + */ + char SLASH = '/'; + /** + * 字符常量:反斜杠 {@code '\\'} + */ + char BACKSLASH = '\\'; + /** + * 字符常量:回车符 {@code '\r'} + */ + char CR = '\r'; + /** + * 字符常量:换行符 {@code '\n'} + */ + char LF = '\n'; + /** + * 字符常量:减号(连接符) {@code '-'} + */ + char DASHED = '-'; + /** + * 字符常量:下划线 {@code '_'} + */ + char UNDERLINE = '_'; + /** + * 字符常量:逗号 {@code ','} + */ + char COMMA = ','; + /** + * 字符常量:花括号(左) '{' + */ + char DELIM_START = '{'; + /** + * 字符常量:花括号(右) '}' + */ + char DELIM_END = '}'; + /** + * 字符常量:中括号(左) {@code '['} + */ + char BRACKET_START = '['; + /** + * 字符常量:中括号(右) {@code ']'} + */ + char BRACKET_END = ']'; + /** + * 字符常量:双引号 {@code '"'} + */ + char DOUBLE_QUOTES = '"'; + /** + * 字符常量:单引号 {@code '\''} + */ + char SINGLE_QUOTE = '\''; + /** + * 字符常量:与 {@code '&'} + */ + char AMP = '&'; + /** + * 字符常量:冒号 {@code ':'} + */ + char COLON = ':'; + /** + * 字符常量:艾特 {@code '@'} + */ + char AT = '@'; +} diff --git a/src/main/java/cn/hutool/core/text/CharSequenceUtil.java b/src/main/java/cn/hutool/core/text/CharSequenceUtil.java new file mode 100644 index 0000000..0c251fe --- /dev/null +++ b/src/main/java/cn/hutool/core/text/CharSequenceUtil.java @@ -0,0 +1,4589 @@ +package cn.hutool.core.text; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.comparator.VersionComparator; +import cn.hutool.core.convert.Convert; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.lang.Filter; +import cn.hutool.core.lang.Matcher; +import cn.hutool.core.lang.func.Func1; +import cn.hutool.core.text.finder.CharFinder; +import cn.hutool.core.text.finder.Finder; +import cn.hutool.core.text.finder.StrFinder; +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.CharUtil; +import cn.hutool.core.util.CharsetUtil; +import cn.hutool.core.util.DesensitizedUtil; +import cn.hutool.core.util.NumberUtil; +import cn.hutool.core.util.ReUtil; +import cn.hutool.core.util.StrUtil; + +import java.nio.ByteBuffer; +import java.nio.charset.Charset; +import java.text.MessageFormat; +import java.text.Normalizer; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; +import java.util.function.Function; +import java.util.function.Predicate; + +/** + * {@link CharSequence} 相关工具类封装 + * + * @author looly + * @since 5.5.3 + */ +public class CharSequenceUtil { + + public static final int INDEX_NOT_FOUND = Finder.INDEX_NOT_FOUND; + + /** + * 字符串常量:{@code "null"}
+ * 注意:{@code "null" != null} + */ + public static final String NULL = "null"; + + /** + * 字符串常量:空字符串 {@code ""} + */ + public static final String EMPTY = ""; + + /** + * 字符串常量:空格符 {@code " "} + */ + public static final String SPACE = " "; + + /** + *

字符串是否为空白,空白的定义如下:

+ *
    + *
  1. {@code null}
  2. + *
  3. 空字符串:{@code ""}
  4. + *
  5. 空格、全角空格、制表符、换行符,等不可见字符
  6. + *
+ * + *

例:

+ *
    + *
  • {@code StrUtil.isBlank(null) // true}
  • + *
  • {@code StrUtil.isBlank("") // true}
  • + *
  • {@code StrUtil.isBlank(" \t\n") // true}
  • + *
  • {@code StrUtil.isBlank("abc") // false}
  • + *
+ * + *

注意:该方法与 {@link #isEmpty(CharSequence)} 的区别是: + * 该方法会校验空白字符,且性能相对于 {@link #isEmpty(CharSequence)} 略慢。

+ *
+ * + *

建议:

+ *
    + *
  • 该方法建议仅对于客户端(或第三方接口)传入的参数使用该方法。
  • + *
  • 需要同时校验多个字符串时,建议采用 {@link #hasBlank(CharSequence...)} 或 {@link #isAllBlank(CharSequence...)}
  • + *
+ * + * @param str 被检测的字符串 + * @return 若为空白,则返回 true + * @see #isEmpty(CharSequence) + */ + public static boolean isBlank(CharSequence str) { + final int length; + if ((str == null) || ((length = str.length()) == 0)) { + return true; + } + + for (int i = 0; i < length; i++) { + // 只要有一个非空字符即为非空字符串 + if (!CharUtil.isBlankChar(str.charAt(i))) { + return false; + } + } + + return true; + } + + /** + *

字符串是否为非空白,非空白的定义如下:

+ *
    + *
  1. 不为 {@code null}
  2. + *
  3. 不为空字符串:{@code ""}
  4. + *
  5. 不为空格、全角空格、制表符、换行符,等不可见字符
  6. + *
+ * + *

例:

+ *
    + *
  • {@code StrUtil.isNotBlank(null) // false}
  • + *
  • {@code StrUtil.isNotBlank("") // false}
  • + *
  • {@code StrUtil.isNotBlank(" \t\n") // false}
  • + *
  • {@code StrUtil.isNotBlank("abc") // true}
  • + *
+ * + *

注意:该方法与 {@link #isNotEmpty(CharSequence)} 的区别是: + * 该方法会校验空白字符,且性能相对于 {@link #isNotEmpty(CharSequence)} 略慢。

+ *

建议:仅对于客户端(或第三方接口)传入的参数使用该方法。

+ * + * @param str 被检测的字符串 + * @return 是否为非空 + * @see #isBlank(CharSequence) + */ + public static boolean isNotBlank(CharSequence str) { + return !isBlank(str); + } + + /** + *

指定字符串数组中,是否包含空字符串。

+ *

如果指定的字符串数组的长度为 0,或者其中的任意一个元素是空字符串,则返回 true。

+ *
+ * + *

例:

+ *
    + *
  • {@code StrUtil.hasBlank() // true}
  • + *
  • {@code StrUtil.hasBlank("", null, " ") // true}
  • + *
  • {@code StrUtil.hasBlank("123", " ") // true}
  • + *
  • {@code StrUtil.hasBlank("123", "abc") // false}
  • + *
+ * + *

注意:该方法与 {@link #isAllBlank(CharSequence...)} 的区别在于:

+ *
    + *
  • hasBlank(CharSequence...) 等价于 {@code isBlank(...) || isBlank(...) || ...}
  • + *
  • {@link #isAllBlank(CharSequence...)} 等价于 {@code isBlank(...) && isBlank(...) && ...}
  • + *
+ * + * @param strs 字符串列表 + * @return 是否包含空字符串 + */ + public static boolean hasBlank(CharSequence... strs) { + if (ArrayUtil.isEmpty(strs)) { + return true; + } + + for (CharSequence str : strs) { + if (isBlank(str)) { + return true; + } + } + return false; + } + + /** + *

指定字符串数组中的元素,是否全部为空字符串。

+ *

如果指定的字符串数组的长度为 0,或者所有元素都是空字符串,则返回 true。

+ *
+ * + *

例:

+ *
    + *
  • {@code StrUtil.isAllBlank() // true}
  • + *
  • {@code StrUtil.isAllBlank("", null, " ") // true}
  • + *
  • {@code StrUtil.isAllBlank("123", " ") // false}
  • + *
  • {@code StrUtil.isAllBlank("123", "abc") // false}
  • + *
+ * + *

注意:该方法与 {@link #hasBlank(CharSequence...)} 的区别在于:

+ *
    + *
  • {@link #hasBlank(CharSequence...)} 等价于 {@code isBlank(...) || isBlank(...) || ...}
  • + *
  • isAllBlank(CharSequence...) 等价于 {@code isBlank(...) && isBlank(...) && ...}
  • + *
+ * + * @param strs 字符串列表 + * @return 所有字符串是否为空白 + */ + public static boolean isAllBlank(CharSequence... strs) { + if (ArrayUtil.isEmpty(strs)) { + return true; + } + + for (CharSequence str : strs) { + if (isNotBlank(str)) { + return false; + } + } + return true; + } + + /** + *

字符串是否为空,空的定义如下:

+ *
    + *
  1. {@code null}
  2. + *
  3. 空字符串:{@code ""}
  4. + *
+ * + *

例:

+ *
    + *
  • {@code StrUtil.isEmpty(null) // true}
  • + *
  • {@code StrUtil.isEmpty("") // true}
  • + *
  • {@code StrUtil.isEmpty(" \t\n") // false}
  • + *
  • {@code StrUtil.isEmpty("abc") // false}
  • + *
+ * + *

注意:该方法与 {@link #isBlank(CharSequence)} 的区别是:该方法不校验空白字符。

+ *

建议:

+ *
    + *
  • 该方法建议用于工具类或任何可以预期的方法参数的校验中。
  • + *
  • 需要同时校验多个字符串时,建议采用 {@link #hasEmpty(CharSequence...)} 或 {@link #isAllEmpty(CharSequence...)}
  • + *
+ * + * @param str 被检测的字符串 + * @return 是否为空 + * @see #isBlank(CharSequence) + */ + public static boolean isEmpty(CharSequence str) { + return str == null || str.length() == 0; + } + + /** + *

字符串是否为非空白,非空白的定义如下:

+ *
    + *
  1. 不为 {@code null}
  2. + *
  3. 不为空字符串:{@code ""}
  4. + *
+ * + *

例:

+ *
    + *
  • {@code StrUtil.isNotEmpty(null) // false}
  • + *
  • {@code StrUtil.isNotEmpty("") // false}
  • + *
  • {@code StrUtil.isNotEmpty(" \t\n") // true}
  • + *
  • {@code StrUtil.isNotEmpty("abc") // true}
  • + *
+ * + *

注意:该方法与 {@link #isNotBlank(CharSequence)} 的区别是:该方法不校验空白字符。

+ *

建议:该方法建议用于工具类或任何可以预期的方法参数的校验中。

+ * + * @param str 被检测的字符串 + * @return 是否为非空 + * @see #isEmpty(CharSequence) + */ + public static boolean isNotEmpty(CharSequence str) { + return !isEmpty(str); + } + + /** + * 当给定字符串为null时,转换为Empty + * + * @param str 被检查的字符串 + * @return 原字符串或者空串 + * @see #nullToEmpty(CharSequence) + * @since 4.6.3 + */ + public static String emptyIfNull(CharSequence str) { + return nullToEmpty(str); + } + + /** + * 当给定字符串为null时,转换为Empty + * + * @param str 被转换的字符串 + * @return 转换后的字符串 + */ + public static String nullToEmpty(CharSequence str) { + return nullToDefault(str, EMPTY); + } + + /** + * 如果字符串是 {@code null},则返回指定默认字符串,否则返回字符串本身。 + * + *
+	 * nullToDefault(null, "default")  = "default"
+	 * nullToDefault("", "default")    = ""
+	 * nullToDefault("  ", "default")  = "  "
+	 * nullToDefault("bat", "default") = "bat"
+	 * 
+ * + * @param str 要转换的字符串 + * @param defaultStr 默认字符串 + * @return 字符串本身或指定的默认字符串 + */ + public static String nullToDefault(CharSequence str, String defaultStr) { + return (str == null) ? defaultStr : str.toString(); + } + + /** + * 如果字符串是{@code null}或者"",则返回指定默认字符串,否则返回字符串本身。 + * + *
+	 * emptyToDefault(null, "default")  = "default"
+	 * emptyToDefault("", "default")    = "default"
+	 * emptyToDefault("  ", "default")  = "  "
+	 * emptyToDefault("bat", "default") = "bat"
+	 * 
+ * + * @param str 要转换的字符串 + * @param defaultStr 默认字符串 + * @return 字符串本身或指定的默认字符串 + * @since 4.1.0 + */ + public static String emptyToDefault(CharSequence str, String defaultStr) { + return isEmpty(str) ? defaultStr : str.toString(); + } + + /** + * 如果字符串是{@code null}或者""或者空白,则返回指定默认字符串,否则返回字符串本身。 + * + *
+	 * blankToDefault(null, "default")  = "default"
+	 * blankToDefault("", "default")    = "default"
+	 * blankToDefault("  ", "default")  = "default"
+	 * blankToDefault("bat", "default") = "bat"
+	 * 
+ * + * @param str 要转换的字符串 + * @param defaultStr 默认字符串 + * @return 字符串本身或指定的默认字符串 + * @since 4.1.0 + */ + public static String blankToDefault(CharSequence str, String defaultStr) { + return isBlank(str) ? defaultStr : str.toString(); + } + + /** + * 当给定字符串为空字符串时,转换为{@code null} + * + * @param str 被转换的字符串 + * @return 转换后的字符串 + */ + public static String emptyToNull(CharSequence str) { + return isEmpty(str) ? null : str.toString(); + } + + /** + *

是否包含空字符串。

+ *

如果指定的字符串数组的长度为 0,或者其中的任意一个元素是空字符串,则返回 true。

+ *
+ * + *

例:

+ *
    + *
  • {@code StrUtil.hasEmpty() // true}
  • + *
  • {@code StrUtil.hasEmpty("", null) // true}
  • + *
  • {@code StrUtil.hasEmpty("123", "") // true}
  • + *
  • {@code StrUtil.hasEmpty("123", "abc") // false}
  • + *
  • {@code StrUtil.hasEmpty(" ", "\t", "\n") // false}
  • + *
+ * + *

注意:该方法与 {@link #isAllEmpty(CharSequence...)} 的区别在于:

+ *
    + *
  • hasEmpty(CharSequence...) 等价于 {@code isEmpty(...) || isEmpty(...) || ...}
  • + *
  • {@link #isAllEmpty(CharSequence...)} 等价于 {@code isEmpty(...) && isEmpty(...) && ...}
  • + *
+ * + * @param strs 字符串列表 + * @return 是否包含空字符串 + */ + public static boolean hasEmpty(CharSequence... strs) { + if (ArrayUtil.isEmpty(strs)) { + return true; + } + + for (CharSequence str : strs) { + if (isEmpty(str)) { + return true; + } + } + return false; + } + + /** + *

指定字符串数组中的元素,是否全部为空字符串。

+ *

如果指定的字符串数组的长度为 0,或者所有元素都是空字符串,则返回 true。

+ *
+ * + *

例:

+ *
    + *
  • {@code StrUtil.isAllEmpty() // true}
  • + *
  • {@code StrUtil.isAllEmpty("", null) // true}
  • + *
  • {@code StrUtil.isAllEmpty("123", "") // false}
  • + *
  • {@code StrUtil.isAllEmpty("123", "abc") // false}
  • + *
  • {@code StrUtil.isAllEmpty(" ", "\t", "\n") // false}
  • + *
+ * + *

注意:该方法与 {@link #hasEmpty(CharSequence...)} 的区别在于:

+ *
    + *
  • {@link #hasEmpty(CharSequence...)} 等价于 {@code isEmpty(...) || isEmpty(...) || ...}
  • + *
  • isAllEmpty(CharSequence...) 等价于 {@code isEmpty(...) && isEmpty(...) && ...}
  • + *
+ * + * @param strs 字符串列表 + * @return 所有字符串是否为空白 + */ + public static boolean isAllEmpty(CharSequence... strs) { + if (ArrayUtil.isEmpty(strs)) { + return true; + } + + for (CharSequence str : strs) { + if (isNotEmpty(str)) { + return false; + } + } + return true; + } + + /** + *

指定字符串数组中的元素,是否都不为空字符串。

+ *

如果指定的字符串数组的长度不为 0,或者所有元素都不是空字符串,则返回 true。

+ *
+ * + *

例:

+ *
    + *
  • {@code StrUtil.isAllNotEmpty() // false}
  • + *
  • {@code StrUtil.isAllNotEmpty("", null) // false}
  • + *
  • {@code StrUtil.isAllNotEmpty("123", "") // false}
  • + *
  • {@code StrUtil.isAllNotEmpty("123", "abc") // true}
  • + *
  • {@code StrUtil.isAllNotEmpty(" ", "\t", "\n") // true}
  • + *
+ * + *

注意:该方法与 {@link #isAllEmpty(CharSequence...)} 的区别在于:

+ *
    + *
  • {@link #isAllEmpty(CharSequence...)} 等价于 {@code isEmpty(...) && isEmpty(...) && ...}
  • + *
  • isAllNotEmpty(CharSequence...) 等价于 {@code !isEmpty(...) && !isEmpty(...) && ...}
  • + *
+ * + * @param args 字符串数组 + * @return 所有字符串是否都不为为空白 + * @since 5.3.6 + */ + public static boolean isAllNotEmpty(CharSequence... args) { + return !hasEmpty(args); + } + + /** + * 是否存都不为{@code null}或空对象或空白符的对象,通过{@link #hasBlank(CharSequence...)} 判断元素 + * + * @param args 被检查的对象,一个或者多个 + * @return 是否都不为空 + * @since 5.3.6 + */ + public static boolean isAllNotBlank(CharSequence... args) { + return !hasBlank(args); + } + + /** + * 检查字符串是否为null、“null”、“undefined” + * + * @param str 被检查的字符串 + * @return 是否为null、“null”、“undefined” + * @since 4.0.10 + */ + public static boolean isNullOrUndefined(CharSequence str) { + if (null == str) { + return true; + } + return isNullOrUndefinedStr(str); + } + + /** + * 检查字符串是否为null、“”、“null”、“undefined” + * + * @param str 被检查的字符串 + * @return 是否为null、“”、“null”、“undefined” + * @since 4.0.10 + */ + public static boolean isEmptyOrUndefined(CharSequence str) { + if (isEmpty(str)) { + return true; + } + return isNullOrUndefinedStr(str); + } + + /** + * 检查字符串是否为null、空白串、“null”、“undefined” + * + * @param str 被检查的字符串 + * @return 是否为null、空白串、“null”、“undefined” + * @since 4.0.10 + */ + public static boolean isBlankOrUndefined(CharSequence str) { + if (isBlank(str)) { + return true; + } + return isNullOrUndefinedStr(str); + } + + /** + * 是否为“null”、“undefined”,不做空指针检查 + * + * @param str 字符串 + * @return 是否为“null”、“undefined” + */ + private static boolean isNullOrUndefinedStr(CharSequence str) { + String strString = str.toString().trim(); + return NULL.equals(strString) || "undefined".equals(strString); + } + + // ------------------------------------------------------------------------ Trim + + /** + * 除去字符串头尾部的空白,如果字符串是{@code null},依然返回{@code null}。 + * + *

+ * 注意,和{@link String#trim()}不同,此方法使用{@link CharUtil#isBlankChar(char)} 来判定空白, 因而可以除去英文字符集之外的其它空白,如中文空格。 + * + *

+	 * trim(null)          = null
+	 * trim("")            = ""
+	 * trim("     ")       = ""
+	 * trim("abc")         = "abc"
+	 * trim("    abc    ") = "abc"
+	 * 
+ * + * @param str 要处理的字符串 + * @return 除去头尾空白的字符串,如果原字串为{@code null},则返回{@code null} + */ + public static String trim(CharSequence str) { + return (null == str) ? null : trim(str, 0); + } + + /** + * 除去字符串头尾部的空白,如果字符串是{@code null},返回{@code ""}。 + * + *
+	 * StrUtil.trimToEmpty(null)          = ""
+	 * StrUtil.trimToEmpty("")            = ""
+	 * StrUtil.trimToEmpty("     ")       = ""
+	 * StrUtil.trimToEmpty("abc")         = "abc"
+	 * StrUtil.trimToEmpty("    abc    ") = "abc"
+	 * 
+ * + * @param str 字符串 + * @return 去除两边空白符后的字符串, 如果为null返回"" + * @since 3.1.1 + */ + public static String trimToEmpty(CharSequence str) { + return str == null ? EMPTY : trim(str); + } + + /** + * 除去字符串头尾部的空白,如果字符串是{@code null}或者"",返回{@code null}。 + * + *
+	 * StrUtil.trimToNull(null)          = null
+	 * StrUtil.trimToNull("")            = null
+	 * StrUtil.trimToNull("     ")       = null
+	 * StrUtil.trimToNull("abc")         = "abc"
+	 * StrUtil.trimToEmpty("    abc    ") = "abc"
+	 * 
+ * + * @param str 字符串 + * @return 去除两边空白符后的字符串, 如果为空返回null + * @since 3.2.1 + */ + public static String trimToNull(CharSequence str) { + final String trimStr = trim(str); + return EMPTY.equals(trimStr) ? null : trimStr; + } + + /** + * 除去字符串头部的空白,如果字符串是{@code null},则返回{@code null}。 + * + *

+ * 注意,和{@link String#trim()}不同,此方法使用{@link CharUtil#isBlankChar(char)} 来判定空白, 因而可以除去英文字符集之外的其它空白,如中文空格。 + * + *

+	 * trimStart(null)         = null
+	 * trimStart("")           = ""
+	 * trimStart("abc")        = "abc"
+	 * trimStart("  abc")      = "abc"
+	 * trimStart("abc  ")      = "abc  "
+	 * trimStart(" abc ")      = "abc "
+	 * 
+ * + * @param str 要处理的字符串 + * @return 除去空白的字符串,如果原字串为{@code null}或结果字符串为{@code ""},则返回 {@code null} + */ + public static String trimStart(CharSequence str) { + return trim(str, -1); + } + + /** + * 除去字符串尾部的空白,如果字符串是{@code null},则返回{@code null}。 + * + *

+ * 注意,和{@link String#trim()}不同,此方法使用{@link CharUtil#isBlankChar(char)} 来判定空白, 因而可以除去英文字符集之外的其它空白,如中文空格。 + * + *

+	 * trimEnd(null)       = null
+	 * trimEnd("")         = ""
+	 * trimEnd("abc")      = "abc"
+	 * trimEnd("  abc")    = "  abc"
+	 * trimEnd("abc  ")    = "abc"
+	 * trimEnd(" abc ")    = " abc"
+	 * 
+ * + * @param str 要处理的字符串 + * @return 除去空白的字符串,如果原字串为{@code null}或结果字符串为{@code ""},则返回 {@code null} + */ + public static String trimEnd(CharSequence str) { + return trim(str, 1); + } + + /** + * 除去字符串头尾部的空白符,如果字符串是{@code null},依然返回{@code null}。 + * + * @param str 要处理的字符串 + * @param mode {@code -1}表示trimStart,{@code 0}表示trim全部, {@code 1}表示trimEnd + * @return 除去指定字符后的的字符串,如果原字串为{@code null},则返回{@code null} + */ + public static String trim(CharSequence str, int mode) { + return trim(str, mode, CharUtil::isBlankChar); + } + + /** + * 按照断言,除去字符串头尾部的断言为真的字符,如果字符串是{@code null},依然返回{@code null}。 + * + * @param str 要处理的字符串 + * @param mode {@code -1}表示trimStart,{@code 0}表示trim全部, {@code 1}表示trimEnd + * @param predicate 断言是否过掉字符,返回{@code true}表述过滤掉,{@code false}表示不过滤 + * @return 除去指定字符后的的字符串,如果原字串为{@code null},则返回{@code null} + * @since 5.7.4 + */ + public static String trim(CharSequence str, int mode, Predicate predicate) { + String result; + if (str == null) { + result = null; + } else { + int length = str.length(); + int start = 0; + int end = length;// 扫描字符串头部 + if (mode <= 0) { + while ((start < end) && (predicate.test(str.charAt(start)))) { + start++; + } + }// 扫描字符串尾部 + if (mode >= 0) { + while ((start < end) && (predicate.test(str.charAt(end - 1)))) { + end--; + } + } + if ((start > 0) || (end < length)) { + result = str.toString().substring(start, end); + } else { + result = str.toString(); + } + } + + return result; + } + + // ------------------------------------------------------------------------ startWith + + /** + * 字符串是否以给定字符开始 + * + * @param str 字符串 + * @param c 字符 + * @return 是否开始 + */ + public static boolean startWith(CharSequence str, char c) { + if (isEmpty(str)) { + return false; + } + return c == str.charAt(0); + } + + /** + * 是否以指定字符串开头
+ * 如果给定的字符串和开头字符串都为null则返回true,否则任意一个值为null返回false + * + * @param str 被监测字符串 + * @param prefix 开头字符串 + * @param ignoreCase 是否忽略大小写 + * @return 是否以指定字符串开头 + * @since 5.4.3 + */ + public static boolean startWith(CharSequence str, CharSequence prefix, boolean ignoreCase) { + return startWith(str, prefix, ignoreCase, false); + } + + /** + * 是否以指定字符串开头
+ * 如果给定的字符串和开头字符串都为null则返回true,否则任意一个值为null返回false
+ *
+	 *     CharSequenceUtil.startWith("123", "123", false, true);   -- false
+	 *     CharSequenceUtil.startWith("ABCDEF", "abc", true, true); -- true
+	 *     CharSequenceUtil.startWith("abc", "abc", true, true);    -- false
+	 * 
+ * + * @param str 被监测字符串 + * @param prefix 开头字符串 + * @param ignoreCase 是否忽略大小写 + * @param ignoreEquals 是否忽略字符串相等的情况 + * @return 是否以指定字符串开头 + * @since 5.4.3 + */ + public static boolean startWith(CharSequence str, CharSequence prefix, boolean ignoreCase, boolean ignoreEquals) { + if (null == str || null == prefix) { + if (ignoreEquals) { + return false; + } + return null == str && null == prefix; + } + + boolean isStartWith = str.toString() + .regionMatches(ignoreCase, 0, prefix.toString(), 0, prefix.length()); + + if (isStartWith) { + return (!ignoreEquals) || (!equals(str, prefix, ignoreCase)); + } + return false; + } + + /** + * 是否以指定字符串开头 + * + * @param str 被监测字符串 + * @param prefix 开头字符串 + * @return 是否以指定字符串开头 + */ + public static boolean startWith(CharSequence str, CharSequence prefix) { + return startWith(str, prefix, false); + } + + /** + * 是否以指定字符串开头,忽略相等字符串的情况 + * + * @param str 被监测字符串 + * @param prefix 开头字符串 + * @return 是否以指定字符串开头并且两个字符串不相等 + */ + public static boolean startWithIgnoreEquals(CharSequence str, CharSequence prefix) { + return startWith(str, prefix, false, true); + } + + /** + * 是否以指定字符串开头,忽略大小写 + * + * @param str 被监测字符串 + * @param prefix 开头字符串 + * @return 是否以指定字符串开头 + */ + public static boolean startWithIgnoreCase(CharSequence str, CharSequence prefix) { + return startWith(str, prefix, true); + } + + /** + * 给定字符串是否以任何一个字符串开始
+ * 给定字符串和数组为空都返回false + * + * @param str 给定字符串 + * @param prefixes 需要检测的开始字符串 + * @return 给定字符串是否以任何一个字符串开始 + * @since 3.0.6 + */ + public static boolean startWithAny(CharSequence str, CharSequence... prefixes) { + if (isEmpty(str) || ArrayUtil.isEmpty(prefixes)) { + return false; + } + + for (CharSequence suffix : prefixes) { + if (startWith(str, suffix, false)) { + return true; + } + } + return false; + } + + /** + * 给定字符串是否以任何一个字符串结尾(忽略大小写)
+ * 给定字符串和数组为空都返回false + * + * @param str 给定字符串 + * @param suffixes 需要检测的结尾字符串 + * @return 给定字符串是否以任何一个字符串结尾 + * @since 5.8.1 + */ + public static boolean startWithAnyIgnoreCase(final CharSequence str, final CharSequence... suffixes) { + if (isEmpty(str) || ArrayUtil.isEmpty(suffixes)) { + return false; + } + + for (final CharSequence suffix : suffixes) { + if (startWith(str, suffix, true)) { + return true; + } + } + return false; + } + + // ------------------------------------------------------------------------ endWith + + /** + * 字符串是否以给定字符结尾 + * + * @param str 字符串 + * @param c 字符 + * @return 是否结尾 + */ + public static boolean endWith(CharSequence str, char c) { + if (isEmpty(str)) { + return false; + } + return c == str.charAt(str.length() - 1); + } + + /** + * 是否以指定字符串结尾
+ * 如果给定的字符串和开头字符串都为null则返回true,否则任意一个值为null返回false + * + * @param str 被监测字符串 + * @param suffix 结尾字符串 + * @param ignoreCase 是否忽略大小写 + * @return 是否以指定字符串结尾 + */ + public static boolean endWith(CharSequence str, CharSequence suffix, boolean ignoreCase) { + return endWith(str, suffix, ignoreCase, false); + } + + /** + * 是否以指定字符串结尾
+ * 如果给定的字符串和开头字符串都为null则返回true,否则任意一个值为null返回false + * + * @param str 被监测字符串 + * @param suffix 结尾字符串 + * @param ignoreCase 是否忽略大小写 + * @param ignoreEquals 是否忽略字符串相等的情况 + * @return 是否以指定字符串结尾 + * @since 5.8.0 + */ + public static boolean endWith(CharSequence str, CharSequence suffix, boolean ignoreCase, boolean ignoreEquals) { + if (null == str || null == suffix) { + if (ignoreEquals) { + return false; + } + return null == str && null == suffix; + } + + final int strOffset = str.length() - suffix.length(); + boolean isEndWith = str.toString() + .regionMatches(ignoreCase, strOffset, suffix.toString(), 0, suffix.length()); + + if (isEndWith) { + return (!ignoreEquals) || (!equals(str, suffix, ignoreCase)); + } + return false; + } + + /** + * 是否以指定字符串结尾 + * + * @param str 被监测字符串 + * @param suffix 结尾字符串 + * @return 是否以指定字符串结尾 + */ + public static boolean endWith(CharSequence str, CharSequence suffix) { + return endWith(str, suffix, false); + } + + /** + * 是否以指定字符串结尾,忽略大小写 + * + * @param str 被监测字符串 + * @param suffix 结尾字符串 + * @return 是否以指定字符串结尾 + */ + public static boolean endWithIgnoreCase(CharSequence str, CharSequence suffix) { + return endWith(str, suffix, true); + } + + /** + * 给定字符串是否以任何一个字符串结尾
+ * 给定字符串和数组为空都返回false + * + * @param str 给定字符串 + * @param suffixes 需要检测的结尾字符串 + * @return 给定字符串是否以任何一个字符串结尾 + * @since 3.0.6 + */ + public static boolean endWithAny(CharSequence str, CharSequence... suffixes) { + if (isEmpty(str) || ArrayUtil.isEmpty(suffixes)) { + return false; + } + + for (CharSequence suffix : suffixes) { + if (endWith(str, suffix, false)) { + return true; + } + } + return false; + } + + /** + * 给定字符串是否以任何一个字符串结尾(忽略大小写)
+ * 给定字符串和数组为空都返回false + * + * @param str 给定字符串 + * @param suffixes 需要检测的结尾字符串 + * @return 给定字符串是否以任何一个字符串结尾 + * @since 5.5.9 + */ + public static boolean endWithAnyIgnoreCase(CharSequence str, CharSequence... suffixes) { + if (isEmpty(str) || ArrayUtil.isEmpty(suffixes)) { + return false; + } + + for (CharSequence suffix : suffixes) { + if (endWith(str, suffix, true)) { + return true; + } + } + return false; + } + + // ------------------------------------------------------------------------ contains + + /** + * 指定字符是否在字符串中出现过 + * + * @param str 字符串 + * @param searchChar 被查找的字符 + * @return 是否包含 + * @since 3.1.2 + */ + public static boolean contains(CharSequence str, char searchChar) { + return indexOf(str, searchChar) > -1; + } + + /** + * 指定字符串是否在字符串中出现过 + * + * @param str 字符串 + * @param searchStr 被查找的字符串 + * @return 是否包含 + * @since 5.1.1 + */ + public static boolean contains(CharSequence str, CharSequence searchStr) { + if (null == str || null == searchStr) { + return false; + } + return str.toString().contains(searchStr); + } + + /** + * 查找指定字符串是否包含指定字符串列表中的任意一个字符串 + * + * @param str 指定字符串 + * @param testStrs 需要检查的字符串数组 + * @return 是否包含任意一个字符串 + * @since 3.2.0 + */ + public static boolean containsAny(CharSequence str, CharSequence... testStrs) { + return null != getContainsStr(str, testStrs); + } + + /** + * 查找指定字符串是否包含指定字符列表中的任意一个字符 + * + * @param str 指定字符串 + * @param testChars 需要检查的字符数组 + * @return 是否包含任意一个字符 + * @since 4.1.11 + */ + public static boolean containsAny(CharSequence str, char... testChars) { + if (!isEmpty(str)) { + int len = str.length(); + for (int i = 0; i < len; i++) { + if (ArrayUtil.contains(testChars, str.charAt(i))) { + return true; + } + } + } + return false; + } + + /** + * 检查指定字符串中是否只包含给定的字符 + * + * @param str 字符串 + * @param testChars 检查的字符 + * @return 字符串含有非检查的字符,返回false + * @since 4.4.1 + */ + public static boolean containsOnly(CharSequence str, char... testChars) { + if (!isEmpty(str)) { + int len = str.length(); + for (int i = 0; i < len; i++) { + if (!ArrayUtil.contains(testChars, str.charAt(i))) { + return false; + } + } + } + return true; + } + + /** + * 检查指定字符串中是否含给定的所有字符串 + * + * @param str 字符串 + * @param testChars 检查的字符 + * @return 字符串含有非检查的字符,返回false + * @since 4.4.1 + */ + public static boolean containsAll(CharSequence str, CharSequence... testChars) { + if (isBlank(str) || ArrayUtil.isEmpty(testChars)) { + return false; + } + for (CharSequence testChar : testChars) { + if (!contains(str, testChar)) { + return false; + } + } + return true; + } + + /** + * 给定字符串是否包含空白符(空白符包括空格、制表符、全角空格和不间断空格)
+ * 如果给定字符串为null或者"",则返回false + * + * @param str 字符串 + * @return 是否包含空白符 + * @since 4.0.8 + */ + public static boolean containsBlank(CharSequence str) { + if (null == str) { + return false; + } + final int length = str.length(); + if (0 == length) { + return false; + } + + for (int i = 0; i < length; i += 1) { + if (CharUtil.isBlankChar(str.charAt(i))) { + return true; + } + } + return false; + } + + /** + * 查找指定字符串是否包含指定字符串列表中的任意一个字符串,如果包含返回找到的第一个字符串 + * + * @param str 指定字符串 + * @param testStrs 需要检查的字符串数组 + * @return 被包含的第一个字符串 + * @since 3.2.0 + */ + public static String getContainsStr(CharSequence str, CharSequence... testStrs) { + if (isEmpty(str) || ArrayUtil.isEmpty(testStrs)) { + return null; + } + for (CharSequence checkStr : testStrs) { + if (str.toString().contains(checkStr)) { + return checkStr.toString(); + } + } + return null; + } + + /** + * 是否包含特定字符,忽略大小写,如果给定两个参数都为{@code null},返回true + * + * @param str 被检测字符串 + * @param testStr 被测试是否包含的字符串 + * @return 是否包含 + */ + public static boolean containsIgnoreCase(CharSequence str, CharSequence testStr) { + if (null == str) { + // 如果被监测字符串和 + return null == testStr; + } + return indexOfIgnoreCase(str, testStr) > -1; + } + + /** + * 查找指定字符串是否包含指定字符串列表中的任意一个字符串
+ * 忽略大小写 + * + * @param str 指定字符串 + * @param testStrs 需要检查的字符串数组 + * @return 是否包含任意一个字符串 + * @since 3.2.0 + */ + public static boolean containsAnyIgnoreCase(CharSequence str, CharSequence... testStrs) { + return null != getContainsStrIgnoreCase(str, testStrs); + } + + /** + * 查找指定字符串是否包含指定字符串列表中的任意一个字符串,如果包含返回找到的第一个字符串
+ * 忽略大小写 + * + * @param str 指定字符串 + * @param testStrs 需要检查的字符串数组 + * @return 被包含的第一个字符串 + * @since 3.2.0 + */ + public static String getContainsStrIgnoreCase(CharSequence str, CharSequence... testStrs) { + if (isEmpty(str) || ArrayUtil.isEmpty(testStrs)) { + return null; + } + for (CharSequence testStr : testStrs) { + if (containsIgnoreCase(str, testStr)) { + return testStr.toString(); + } + } + return null; + } + + // ------------------------------------------------------------------------ indexOf + + /** + * 指定范围内查找指定字符 + * + * @param str 字符串 + * @param searchChar 被查找的字符 + * @return 位置 + */ + public static int indexOf(CharSequence str, char searchChar) { + return indexOf(str, searchChar, 0); + } + + /** + * 指定范围内查找指定字符 + * + * @param str 字符串 + * @param searchChar 被查找的字符 + * @param start 起始位置,如果小于0,从0开始查找 + * @return 位置 + */ + public static int indexOf(CharSequence str, char searchChar, int start) { + if (str instanceof String) { + return ((String) str).indexOf(searchChar, start); + } else { + return indexOf(str, searchChar, start, -1); + } + } + + /** + * 指定范围内查找指定字符 + * + * @param text 字符串 + * @param searchChar 被查找的字符 + * @param start 起始位置,如果小于0,从0开始查找 + * @param end 终止位置,如果超过str.length()则默认查找到字符串末尾 + * @return 位置 + */ + public static int indexOf(CharSequence text, char searchChar, int start, int end) { + if (isEmpty(text)) { + return INDEX_NOT_FOUND; + } + return new CharFinder(searchChar).setText(text).setEndIndex(end).start(start); + } + + /** + * 指定范围内查找字符串,忽略大小写
+ * + *
+	 * StrUtil.indexOfIgnoreCase(null, *, *)          = -1
+	 * StrUtil.indexOfIgnoreCase(*, null, *)          = -1
+	 * StrUtil.indexOfIgnoreCase("", "", 0)           = 0
+	 * StrUtil.indexOfIgnoreCase("aabaabaa", "A", 0)  = 0
+	 * StrUtil.indexOfIgnoreCase("aabaabaa", "B", 0)  = 2
+	 * StrUtil.indexOfIgnoreCase("aabaabaa", "AB", 0) = 1
+	 * StrUtil.indexOfIgnoreCase("aabaabaa", "B", 3)  = 5
+	 * StrUtil.indexOfIgnoreCase("aabaabaa", "B", 9)  = -1
+	 * StrUtil.indexOfIgnoreCase("aabaabaa", "B", -1) = 2
+	 * StrUtil.indexOfIgnoreCase("aabaabaa", "", 2)   = 2
+	 * StrUtil.indexOfIgnoreCase("abc", "", 9)        = -1
+	 * 
+ * + * @param str 字符串 + * @param searchStr 需要查找位置的字符串 + * @return 位置 + * @since 3.2.1 + */ + public static int indexOfIgnoreCase(final CharSequence str, final CharSequence searchStr) { + return indexOfIgnoreCase(str, searchStr, 0); + } + + /** + * 指定范围内查找字符串 + * + *
+	 * StrUtil.indexOfIgnoreCase(null, *, *)          = -1
+	 * StrUtil.indexOfIgnoreCase(*, null, *)          = -1
+	 * StrUtil.indexOfIgnoreCase("", "", 0)           = 0
+	 * StrUtil.indexOfIgnoreCase("aabaabaa", "A", 0)  = 0
+	 * StrUtil.indexOfIgnoreCase("aabaabaa", "B", 0)  = 2
+	 * StrUtil.indexOfIgnoreCase("aabaabaa", "AB", 0) = 1
+	 * StrUtil.indexOfIgnoreCase("aabaabaa", "B", 3)  = 5
+	 * StrUtil.indexOfIgnoreCase("aabaabaa", "B", 9)  = -1
+	 * StrUtil.indexOfIgnoreCase("aabaabaa", "B", -1) = 2
+	 * StrUtil.indexOfIgnoreCase("aabaabaa", "", 2)   = 2
+	 * StrUtil.indexOfIgnoreCase("abc", "", 9)        = -1
+	 * 
+ * + * @param str 字符串 + * @param searchStr 需要查找位置的字符串 + * @param fromIndex 起始位置 + * @return 位置 + * @since 3.2.1 + */ + public static int indexOfIgnoreCase(final CharSequence str, final CharSequence searchStr, int fromIndex) { + return indexOf(str, searchStr, fromIndex, true); + } + + /** + * 指定范围内查找字符串 + * + * @param text 字符串,空则返回-1 + * @param searchStr 需要查找位置的字符串,空则返回-1 + * @param from 起始位置(包含) + * @param ignoreCase 是否忽略大小写 + * @return 位置 + * @since 3.2.1 + */ + public static int indexOf(CharSequence text, CharSequence searchStr, int from, boolean ignoreCase) { + if (isEmpty(text) || isEmpty(searchStr)) { + if (StrUtil.equals(text, searchStr)) { + return 0; + } else { + return INDEX_NOT_FOUND; + } + } + return new StrFinder(searchStr, ignoreCase).setText(text).start(from); + } + + /** + * 指定范围内查找字符串,忽略大小写 + * + * @param str 字符串 + * @param searchStr 需要查找位置的字符串 + * @return 位置 + * @since 3.2.1 + */ + public static int lastIndexOfIgnoreCase(CharSequence str, CharSequence searchStr) { + return lastIndexOfIgnoreCase(str, searchStr, str.length()); + } + + /** + * 指定范围内查找字符串,忽略大小写
+ * fromIndex 为搜索起始位置,从后往前计数 + * + * @param str 字符串 + * @param searchStr 需要查找位置的字符串 + * @param fromIndex 起始位置,从后往前计数 + * @return 位置 + * @since 3.2.1 + */ + public static int lastIndexOfIgnoreCase(CharSequence str, CharSequence searchStr, int fromIndex) { + return lastIndexOf(str, searchStr, fromIndex, true); + } + + /** + * 指定范围内查找字符串
+ * fromIndex 为搜索起始位置,从后往前计数 + * + * @param text 字符串 + * @param searchStr 需要查找位置的字符串 + * @param from 起始位置,从后往前计数 + * @param ignoreCase 是否忽略大小写 + * @return 位置 + * @since 3.2.1 + */ + public static int lastIndexOf(CharSequence text, CharSequence searchStr, int from, boolean ignoreCase) { + if (isEmpty(text) || isEmpty(searchStr)) { + if (StrUtil.equals(text, searchStr)) { + return 0; + } else { + return INDEX_NOT_FOUND; + } + } + return new StrFinder(searchStr, ignoreCase) + .setText(text).setNegative(true).start(from); + } + + /** + * 返回字符串 searchStr 在字符串 str 中第 ordinal 次出现的位置。 + * + *

+ * 如果 str=null 或 searchStr=null 或 ordinal≥0 则返回-1
+ * 此方法来自:Apache-Commons-Lang + *

+ * 例子(*代表任意字符): + * + *

+	 * StrUtil.ordinalIndexOf(null, *, *)          = -1
+	 * StrUtil.ordinalIndexOf(*, null, *)          = -1
+	 * StrUtil.ordinalIndexOf("", "", *)           = 0
+	 * StrUtil.ordinalIndexOf("aabaabaa", "a", 1)  = 0
+	 * StrUtil.ordinalIndexOf("aabaabaa", "a", 2)  = 1
+	 * StrUtil.ordinalIndexOf("aabaabaa", "b", 1)  = 2
+	 * StrUtil.ordinalIndexOf("aabaabaa", "b", 2)  = 5
+	 * StrUtil.ordinalIndexOf("aabaabaa", "ab", 1) = 1
+	 * StrUtil.ordinalIndexOf("aabaabaa", "ab", 2) = 4
+	 * StrUtil.ordinalIndexOf("aabaabaa", "", 1)   = 0
+	 * StrUtil.ordinalIndexOf("aabaabaa", "", 2)   = 0
+	 * 
+ * + * @param str 被检查的字符串,可以为null + * @param searchStr 被查找的字符串,可以为null + * @param ordinal 第几次出现的位置 + * @return 查找到的位置 + * @since 3.2.3 + */ + public static int ordinalIndexOf(CharSequence str, CharSequence searchStr, int ordinal) { + if (str == null || searchStr == null || ordinal <= 0) { + return INDEX_NOT_FOUND; + } + if (searchStr.length() == 0) { + return 0; + } + int found = 0; + int index = INDEX_NOT_FOUND; + do { + index = indexOf(str, searchStr, index + 1, false); + if (index < 0) { + return index; + } + found++; + } while (found < ordinal); + return index; + } + + // ------------------------------------------------------------------------ remove + + /** + * 移除字符串中所有给定字符串
+ * 例:removeAll("aa-bb-cc-dd", "-") =》 aabbccdd + * + * @param str 字符串 + * @param strToRemove 被移除的字符串 + * @return 移除后的字符串 + */ + public static String removeAll(CharSequence str, CharSequence strToRemove) { + // strToRemove如果为空, 也不用继续后面的逻辑 + if (isEmpty(str) || isEmpty(strToRemove)) { + return str(str); + } + return str.toString().replace(strToRemove, EMPTY); + } + + /** + * 移除字符串中所有给定字符串,当某个字符串出现多次,则全部移除
+ * 例:removeAny("aa-bb-cc-dd", "a", "b") =》 --cc-dd + * + * @param str 字符串 + * @param strsToRemove 被移除的字符串 + * @return 移除后的字符串 + * @since 5.3.8 + */ + public static String removeAny(CharSequence str, CharSequence... strsToRemove) { + String result = str(str); + if (isNotEmpty(str)) { + for (CharSequence strToRemove : strsToRemove) { + result = removeAll(result, strToRemove); + } + } + return result; + } + + /** + * 去除字符串中指定的多个字符,如有多个则全部去除 + * + * @param str 字符串 + * @param chars 字符列表 + * @return 去除后的字符 + * @since 4.2.2 + */ + public static String removeAll(CharSequence str, char... chars) { + if (null == str || ArrayUtil.isEmpty(chars)) { + return str(str); + } + final int len = str.length(); + if (0 == len) { + return str(str); + } + final StringBuilder builder = new StringBuilder(len); + char c; + for (int i = 0; i < len; i++) { + c = str.charAt(i); + if (!ArrayUtil.contains(chars, c)) { + builder.append(c); + } + } + return builder.toString(); + } + + /** + * 去除所有换行符,包括: + * + *
+	 * 1. \r
+	 * 1. \n
+	 * 
+ * + * @param str 字符串 + * @return 处理后的字符串 + * @since 4.2.2 + */ + public static String removeAllLineBreaks(CharSequence str) { + return removeAll(str, CharUtil.CR, CharUtil.LF); + } + + /** + * 去掉首部指定长度的字符串并将剩余字符串首字母小写
+ * 例如:str=setName, preLength=3 =》 return name + * + * @param str 被处理的字符串 + * @param preLength 去掉的长度 + * @return 处理后的字符串,不符合规范返回null + */ + public static String removePreAndLowerFirst(CharSequence str, int preLength) { + if (str == null) { + return null; + } + if (str.length() > preLength) { + char first = Character.toLowerCase(str.charAt(preLength)); + if (str.length() > preLength + 1) { + return first + str.toString().substring(preLength + 1); + } + return String.valueOf(first); + } else { + return str.toString(); + } + } + + /** + * 去掉首部指定长度的字符串并将剩余字符串首字母小写
+ * 例如:str=setName, prefix=set =》 return name + * + * @param str 被处理的字符串 + * @param prefix 前缀 + * @return 处理后的字符串,不符合规范返回null + */ + public static String removePreAndLowerFirst(CharSequence str, CharSequence prefix) { + return lowerFirst(removePrefix(str, prefix)); + } + + /** + * 去掉指定前缀 + * + * @param str 字符串 + * @param prefix 前缀 + * @return 切掉后的字符串,若前缀不是 preffix, 返回原字符串 + */ + public static String removePrefix(CharSequence str, CharSequence prefix) { + if (isEmpty(str) || isEmpty(prefix)) { + return str(str); + } + + final String str2 = str.toString(); + if (str2.startsWith(prefix.toString())) { + return subSuf(str2, prefix.length());// 截取后半段 + } + return str2; + } + + /** + * 忽略大小写去掉指定前缀 + * + * @param str 字符串 + * @param prefix 前缀 + * @return 切掉后的字符串,若前缀不是 prefix, 返回原字符串 + */ + public static String removePrefixIgnoreCase(CharSequence str, CharSequence prefix) { + if (isEmpty(str) || isEmpty(prefix)) { + return str(str); + } + + final String str2 = str.toString(); + if (startWithIgnoreCase(str, prefix)) { + return subSuf(str2, prefix.length());// 截取后半段 + } + return str2; + } + + /** + * 去掉指定后缀 + * + * @param str 字符串 + * @param suffix 后缀 + * @return 切掉后的字符串,若后缀不是 suffix, 返回原字符串 + */ + public static String removeSuffix(CharSequence str, CharSequence suffix) { + if (isEmpty(str) || isEmpty(suffix)) { + return str(str); + } + + final String str2 = str.toString(); + if (str2.endsWith(suffix.toString())) { + return subPre(str2, str2.length() - suffix.length());// 截取前半段 + } + return str2; + } + + /** + * 去掉指定后缀,并小写首字母 + * + * @param str 字符串 + * @param suffix 后缀 + * @return 切掉后的字符串,若后缀不是 suffix, 返回原字符串 + */ + public static String removeSufAndLowerFirst(CharSequence str, CharSequence suffix) { + return lowerFirst(removeSuffix(str, suffix)); + } + + /** + * 忽略大小写去掉指定后缀 + * + * @param str 字符串 + * @param suffix 后缀 + * @return 切掉后的字符串,若后缀不是 suffix, 返回原字符串 + */ + public static String removeSuffixIgnoreCase(CharSequence str, CharSequence suffix) { + if (isEmpty(str) || isEmpty(suffix)) { + return str(str); + } + + final String str2 = str.toString(); + if (endWithIgnoreCase(str, suffix)) { + return subPre(str2, str2.length() - suffix.length()); + } + return str2; + } + + /** + * 清理空白字符 + * + * @param str 被清理的字符串 + * @return 清理后的字符串 + */ + public static String cleanBlank(CharSequence str) { + return filter(str, c -> !CharUtil.isBlankChar(c)); + } + + // ------------------------------------------------------------------------ strip + + /** + * 去除两边的指定字符串 + * + * @param str 被处理的字符串 + * @param prefixOrSuffix 前缀或后缀 + * @return 处理后的字符串 + * @since 3.1.2 + */ + public static String strip(CharSequence str, CharSequence prefixOrSuffix) { + if (equals(str, prefixOrSuffix)) { + // 对于去除相同字符的情况单独处理 + return EMPTY; + } + return strip(str, prefixOrSuffix, prefixOrSuffix); + } + + /** + * 去除两边的指定字符串 + * + * @param str 被处理的字符串 + * @param prefix 前缀 + * @param suffix 后缀 + * @return 处理后的字符串 + * @since 3.1.2 + */ + public static String strip(CharSequence str, CharSequence prefix, CharSequence suffix) { + if (isEmpty(str)) { + return str(str); + } + + int from = 0; + int to = str.length(); + + String str2 = str.toString(); + if (startWith(str2, prefix)) { + from = prefix.length(); + } + if (endWith(str2, suffix)) { + to -= suffix.length(); + } + + return str2.substring(Math.min(from, to), Math.max(from, to)); + } + + /** + * 去除两边的指定字符串,忽略大小写 + * + * @param str 被处理的字符串 + * @param prefixOrSuffix 前缀或后缀 + * @return 处理后的字符串 + * @since 3.1.2 + */ + public static String stripIgnoreCase(CharSequence str, CharSequence prefixOrSuffix) { + return stripIgnoreCase(str, prefixOrSuffix, prefixOrSuffix); + } + + /** + * 去除两边的指定字符串,忽略大小写 + * + * @param str 被处理的字符串 + * @param prefix 前缀 + * @param suffix 后缀 + * @return 处理后的字符串 + * @since 3.1.2 + */ + public static String stripIgnoreCase(CharSequence str, CharSequence prefix, CharSequence suffix) { + if (isEmpty(str)) { + return str(str); + } + int from = 0; + int to = str.length(); + + String str2 = str.toString(); + if (startWithIgnoreCase(str2, prefix)) { + from = prefix.length(); + } + if (endWithIgnoreCase(str2, suffix)) { + to -= suffix.length(); + } + return str2.substring(from, to); + } + + // ------------------------------------------------------------------------ add + + /** + * 如果给定字符串不是以prefix开头的,在开头补充 prefix + * + * @param str 字符串 + * @param prefix 前缀 + * @return 补充后的字符串 + * @see #prependIfMissing(CharSequence, CharSequence, CharSequence...) + */ + public static String addPrefixIfNot(CharSequence str, CharSequence prefix) { + return prependIfMissing(str, prefix, prefix); + } + + /** + * 如果给定字符串不是以suffix结尾的,在尾部补充 suffix + * + * @param str 字符串 + * @param suffix 后缀 + * @return 补充后的字符串 + * @see #appendIfMissing(CharSequence, CharSequence, CharSequence...) + */ + public static String addSuffixIfNot(CharSequence str, CharSequence suffix) { + return appendIfMissing(str, suffix, suffix); + } + + // ------------------------------------------------------------------------ split + + /** + * 切分字符串为long数组 + * + * @param str 被切分的字符串 + * @param separator 分隔符 + * @return 切分后long数组 + * @since 4.0.6 + */ + public static long[] splitToLong(CharSequence str, char separator) { + return Convert.convert(long[].class, splitTrim(str, separator)); + } + + /** + * 切分字符串为long数组 + * + * @param str 被切分的字符串 + * @param separator 分隔符字符串 + * @return 切分后long数组 + * @since 4.0.6 + */ + public static long[] splitToLong(CharSequence str, CharSequence separator) { + return Convert.convert(long[].class, splitTrim(str, separator)); + } + + /** + * 切分字符串为int数组 + * + * @param str 被切分的字符串 + * @param separator 分隔符 + * @return 切分后long数组 + * @since 4.0.6 + */ + public static int[] splitToInt(CharSequence str, char separator) { + return Convert.convert(int[].class, splitTrim(str, separator)); + } + + /** + * 切分字符串为int数组 + * + * @param str 被切分的字符串 + * @param separator 分隔符字符串 + * @return 切分后long数组 + * @since 4.0.6 + */ + public static int[] splitToInt(CharSequence str, CharSequence separator) { + return Convert.convert(int[].class, splitTrim(str, separator)); + } + + /** + * 切分字符串
+ * a#b#c =》 [a,b,c]
+ * a##b#c =》 [a,"",b,c] + * + * @param str 被切分的字符串 + * @param separator 分隔符字符 + * @return 切分后的集合 + */ + public static List split(CharSequence str, char separator) { + return split(str, separator, 0); + } + + /** + * 切分字符串,如果分隔符不存在则返回原字符串 + * + * @param str 被切分的字符串 + * @param separator 分隔符 + * @return 字符串 + * @since 5.6.7 + */ + public static String[] splitToArray(CharSequence str, CharSequence separator) { + if (str == null) { + return new String[]{}; + } + + return StrSplitter.splitToArray(str.toString(), str(separator), 0, false, false); + } + + /** + * 切分字符串 + * + * @param str 被切分的字符串 + * @param separator 分隔符字符 + * @return 切分后的数组 + */ + public static String[] splitToArray(CharSequence str, char separator) { + return splitToArray(str, separator, 0); + } + + /** + * 切分字符串 + * + * @param text 被切分的字符串 + * @param separator 分隔符字符 + * @param limit 限制分片数 + * @return 切分后的数组 + */ + public static String[] splitToArray(CharSequence text, char separator, int limit) { + Assert.notNull(text, "Text must be not null!"); + return StrSplitter.splitToArray(text.toString(), separator, limit, false, false); + } + + /** + * 切分字符串,不去除切分后每个元素两边的空白符,不去除空白项 + * + * @param str 被切分的字符串 + * @param separator 分隔符字符 + * @param limit 限制分片数,-1不限制 + * @return 切分后的集合 + */ + public static List split(CharSequence str, char separator, int limit) { + return split(str, separator, limit, false, false); + } + + /** + * 切分字符串,去除切分后每个元素两边的空白符,去除空白项 + * + * @param str 被切分的字符串 + * @param separator 分隔符字符 + * @return 切分后的集合 + * @since 3.1.2 + */ + public static List splitTrim(CharSequence str, char separator) { + return splitTrim(str, separator, -1); + } + + /** + * 切分字符串,去除切分后每个元素两边的空白符,去除空白项 + * + * @param str 被切分的字符串 + * @param separator 分隔符字符 + * @return 切分后的集合 + * @since 3.2.0 + */ + public static List splitTrim(CharSequence str, CharSequence separator) { + return splitTrim(str, separator, -1); + } + + /** + * 切分字符串,去除切分后每个元素两边的空白符,去除空白项 + * + * @param str 被切分的字符串 + * @param separator 分隔符字符 + * @param limit 限制分片数,-1不限制 + * @return 切分后的集合 + * @since 3.1.0 + */ + public static List splitTrim(CharSequence str, char separator, int limit) { + return split(str, separator, limit, true, true); + } + + /** + * 切分字符串,去除切分后每个元素两边的空白符,去除空白项 + * + * @param str 被切分的字符串 + * @param separator 分隔符字符 + * @param limit 限制分片数,-1不限制 + * @return 切分后的集合 + * @since 3.2.0 + */ + public static List splitTrim(CharSequence str, CharSequence separator, int limit) { + return split(str, separator, limit, true, true); + } + + /** + * 切分字符串,不限制分片数量 + * + * @param str 被切分的字符串 + * @param separator 分隔符字符 + * @param isTrim 是否去除切分字符串后每个元素两边的空格 + * @param ignoreEmpty 是否忽略空串 + * @return 切分后的集合 + * @since 3.0.8 + */ + public static List split(CharSequence str, char separator, boolean isTrim, boolean ignoreEmpty) { + return split(str, separator, 0, isTrim, ignoreEmpty); + } + + /** + * 切分字符串 + * + * @param str 被切分的字符串 + * @param separator 分隔符字符 + * @param limit 限制分片数,-1不限制 + * @param isTrim 是否去除切分字符串后每个元素两边的空格 + * @param ignoreEmpty 是否忽略空串 + * @return 切分后的集合 + * @since 3.0.8 + */ + public static List split(CharSequence str, char separator, int limit, boolean isTrim, boolean ignoreEmpty) { + return StrSplitter.split(str, separator, limit, isTrim, ignoreEmpty); + } + + /** + * 切分字符串 + * + * @param 切分后元素类型 + * @param str 被切分的字符串 + * @param separator 分隔符字符 + * @param limit 限制分片数,-1不限制 + * @param ignoreEmpty 是否忽略空串 + * @param mapping 切分后的字符串元素的转换方法 + * @return 切分后的集合,元素类型是经过 mapping 转换后的 + * @since 5.7.14 + */ + public static List split(CharSequence str, char separator, int limit, boolean ignoreEmpty, Function mapping) { + return StrSplitter.split(str, separator, limit, ignoreEmpty, mapping); + } + + /** + * 切分字符串,如果分隔符不存在则返回原字符串 + * + * @param str 被切分的字符串 + * @param separator 分隔符 + * @return 字符串 + * @since 5.7.1 + */ + public static List split(CharSequence str, CharSequence separator) { + return split(str, separator, false, false); + } + + /** + * 切分字符串 + * + * @param str 被切分的字符串 + * @param separator 分隔符字符 + * @param isTrim 是否去除切分字符串后每个元素两边的空格 + * @param ignoreEmpty 是否忽略空串 + * @return 切分后的集合 + * @since 5.6.7 + */ + public static List split(CharSequence str, CharSequence separator, boolean isTrim, boolean ignoreEmpty) { + return split(str, separator, 0, isTrim, ignoreEmpty); + } + + /** + * 切分字符串 + * + * @param str 被切分的字符串 + * @param separator 分隔符字符 + * @param limit 限制分片数,-1不限制 + * @param isTrim 是否去除切分字符串后每个元素两边的空格 + * @param ignoreEmpty 是否忽略空串 + * @return 切分后的集合 + * @since 3.2.0 + */ + public static List split(CharSequence str, CharSequence separator, int limit, boolean isTrim, boolean ignoreEmpty) { + final String separatorStr = (null == separator) ? null : separator.toString(); + return StrSplitter.split(str, separatorStr, limit, isTrim, ignoreEmpty); + } + + /** + * 根据给定长度,将给定字符串截取为多个部分 + * + * @param str 字符串 + * @param len 每一个小节的长度 + * @return 截取后的字符串数组 + * @see StrSplitter#splitByLength(CharSequence, int) + */ + public static String[] split(CharSequence str, int len) { + return StrSplitter.splitByLength(str, len); + } + + /** + * 将字符串切分为N等份 + * + * @param str 字符串 + * @param partLength 每等份的长度 + * @return 切分后的数组 + * @since 3.0.6 + */ + public static String[] cut(CharSequence str, int partLength) { + if (null == str) { + return null; + } + int len = str.length(); + if (len < partLength) { + return new String[]{str.toString()}; + } + int part = NumberUtil.count(len, partLength); + final String[] array = new String[part]; + + final String str2 = str.toString(); + for (int i = 0; i < part; i++) { + array[i] = str2.substring(i * partLength, (i == part - 1) ? len : (partLength + i * partLength)); + } + return array; + } + + // ------------------------------------------------------------------------ sub + + /** + * 改进JDK subString
+ * index从0开始计算,最后一个字符为-1
+ * 如果from和to位置一样,返回 ""
+ * 如果from或to为负数,则按照length从后向前数位置,如果绝对值大于字符串长度,则from归到0,to归到length
+ * 如果经过修正的index中from大于to,则互换from和to example:
+ * abcdefgh 2 3 =》 c
+ * abcdefgh 2 -3 =》 cde
+ * + * @param str String + * @param fromIndexInclude 开始的index(包括) + * @param toIndexExclude 结束的index(不包括) + * @return 字串 + */ + public static String sub(CharSequence str, int fromIndexInclude, int toIndexExclude) { + if (isEmpty(str)) { + return str(str); + } + int len = str.length(); + + if (fromIndexInclude < 0) { + fromIndexInclude = len + fromIndexInclude; + if (fromIndexInclude < 0) { + fromIndexInclude = 0; + } + } else if (fromIndexInclude > len) { + fromIndexInclude = len; + } + + if (toIndexExclude < 0) { + toIndexExclude = len + toIndexExclude; + if (toIndexExclude < 0) { + toIndexExclude = len; + } + } else if (toIndexExclude > len) { + toIndexExclude = len; + } + + if (toIndexExclude < fromIndexInclude) { + int tmp = fromIndexInclude; + fromIndexInclude = toIndexExclude; + toIndexExclude = tmp; + } + + if (fromIndexInclude == toIndexExclude) { + return EMPTY; + } + + return str.toString().substring(fromIndexInclude, toIndexExclude); + } + + /** + * 通过CodePoint截取字符串,可以截断Emoji + * + * @param str String + * @param fromIndex 开始的index(包括) + * @param toIndex 结束的index(不包括) + * @return 字串 + */ + public static String subByCodePoint(CharSequence str, int fromIndex, int toIndex) { + if (isEmpty(str)) { + return str(str); + } + + if (fromIndex < 0 || fromIndex > toIndex) { + throw new IllegalArgumentException(); + } + + if (fromIndex == toIndex) { + return EMPTY; + } + + final StringBuilder sb = new StringBuilder(); + final int subLen = toIndex - fromIndex; + str.toString().codePoints().skip(fromIndex).limit(subLen).forEach(v -> sb.append(Character.toChars(v))); + return sb.toString(); + } + + /** + * 截取部分字符串,这里一个汉字的长度认为是2 + * + * @param str 字符串 + * @param len bytes切割到的位置(包含) + * @param suffix 切割后加上后缀 + * @return 切割后的字符串 + * @since 3.1.1 + */ + public static String subPreGbk(CharSequence str, int len, CharSequence suffix) { + return subPreGbk(str, len, true) + suffix; + } + + /** + * 截取部分字符串,这里一个汉字的长度认为是2
+ * 可以自定义halfUp,如len为10,如果截取后最后一个字符是半个字符,{@code true}表示保留,则长度是11,否则长度9 + * + * @param str 字符串 + * @param len bytes切割到的位置(包含) + * @param halfUp 遇到截取一半的GBK字符,是否保留。 + * @return 切割后的字符串 + * @since 5.7.17 + */ + public static String subPreGbk(CharSequence str, int len, boolean halfUp) { + if (isEmpty(str)) { + return str(str); + } + + int counterOfDoubleByte = 0; + final byte[] b = bytes(str, CharsetUtil.CHARSET_GBK); + if (b.length <= len) { + return str.toString(); + } + for (int i = 0; i < len; i++) { + if (b[i] < 0) { + counterOfDoubleByte++; + } + } + + if (counterOfDoubleByte % 2 != 0) { + if (halfUp) { + len += 1; + } else { + len -= 1; + } + } + return new String(b, 0, len, CharsetUtil.CHARSET_GBK); + } + + /** + * 切割指定位置之前部分的字符串 + * + * @param string 字符串 + * @param toIndexExclude 切割到的位置(不包括) + * @return 切割后的剩余的前半部分字符串 + */ + public static String subPre(CharSequence string, int toIndexExclude) { + return sub(string, 0, toIndexExclude); + } + + /** + * 切割指定位置之后部分的字符串 + * + * @param string 字符串 + * @param fromIndex 切割开始的位置(包括) + * @return 切割后后剩余的后半部分字符串 + */ + public static String subSuf(CharSequence string, int fromIndex) { + if (isEmpty(string)) { + return null; + } + return sub(string, fromIndex, string.length()); + } + + /** + * 切割指定长度的后部分的字符串 + * + *
+	 * StrUtil.subSufByLength("abcde", 3)      =    "cde"
+	 * StrUtil.subSufByLength("abcde", 0)      =    ""
+	 * StrUtil.subSufByLength("abcde", -5)     =    ""
+	 * StrUtil.subSufByLength("abcde", -1)     =    ""
+	 * StrUtil.subSufByLength("abcde", 5)       =    "abcde"
+	 * StrUtil.subSufByLength("abcde", 10)     =    "abcde"
+	 * StrUtil.subSufByLength(null, 3)               =    null
+	 * 
+ * + * @param string 字符串 + * @param length 切割长度 + * @return 切割后后剩余的后半部分字符串 + * @since 4.0.1 + */ + public static String subSufByLength(CharSequence string, int length) { + if (isEmpty(string)) { + return null; + } + if (length <= 0) { + return EMPTY; + } + return sub(string, -length, string.length()); + } + + /** + * 截取字符串,从指定位置开始,截取指定长度的字符串
+ * 如果fromIndex为正数,则向后截取指定length长度,如果为负数,则向前截取length长度。 + * + * @param input 原始字符串 + * @param fromIndex 开始的index,包括 + * @param length 要截取的长度 + * @return 截取后的字符串 + * @author weibaohui + */ + public static String subWithLength(String input, int fromIndex, int length) { + final int toIndex; + if(fromIndex < 0){ + toIndex = fromIndex - length; + }else{ + toIndex = fromIndex + length; + } + return sub(input, fromIndex, toIndex); + } + + /** + * 截取分隔字符串之前的字符串,不包括分隔字符串
+ * 如果给定的字符串为空串(null或"")或者分隔字符串为null,返回原字符串
+ * 如果分隔字符串为空串"",则返回空串,如果分隔字符串未找到,返回原字符串,举例如下: + * + *
+	 * StrUtil.subBefore(null, *, false)      = null
+	 * StrUtil.subBefore("", *, false)        = ""
+	 * StrUtil.subBefore("abc", "a", false)   = ""
+	 * StrUtil.subBefore("abcba", "b", false) = "a"
+	 * StrUtil.subBefore("abc", "c", false)   = "ab"
+	 * StrUtil.subBefore("abc", "d", false)   = "abc"
+	 * StrUtil.subBefore("abc", "", false)    = ""
+	 * StrUtil.subBefore("abc", null, false)  = "abc"
+	 * 
+ * + * @param string 被查找的字符串 + * @param separator 分隔字符串(不包括) + * @param isLastSeparator 是否查找最后一个分隔字符串(多次出现分隔字符串时选取最后一个),true为选取最后一个 + * @return 切割后的字符串 + * @since 3.1.1 + */ + public static String subBefore(CharSequence string, CharSequence separator, boolean isLastSeparator) { + if (isEmpty(string) || separator == null) { + return null == string ? null : string.toString(); + } + + final String str = string.toString(); + final String sep = separator.toString(); + if (sep.isEmpty()) { + return EMPTY; + } + final int pos = isLastSeparator ? str.lastIndexOf(sep) : str.indexOf(sep); + if (INDEX_NOT_FOUND == pos) { + return str; + } + if (0 == pos) { + return EMPTY; + } + return str.substring(0, pos); + } + + /** + * 截取分隔字符串之前的字符串,不包括分隔字符串
+ * 如果给定的字符串为空串(null或"")或者分隔字符串为null,返回原字符串
+ * 如果分隔字符串未找到,返回原字符串,举例如下: + * + *
+	 * StrUtil.subBefore(null, *, false)      = null
+	 * StrUtil.subBefore("", *, false)        = ""
+	 * StrUtil.subBefore("abc", 'a', false)   = ""
+	 * StrUtil.subBefore("abcba", 'b', false) = "a"
+	 * StrUtil.subBefore("abc", 'c', false)   = "ab"
+	 * StrUtil.subBefore("abc", 'd', false)   = "abc"
+	 * 
+ * + * @param string 被查找的字符串 + * @param separator 分隔字符串(不包括) + * @param isLastSeparator 是否查找最后一个分隔字符串(多次出现分隔字符串时选取最后一个),true为选取最后一个 + * @return 切割后的字符串 + * @since 4.1.15 + */ + public static String subBefore(CharSequence string, char separator, boolean isLastSeparator) { + if (isEmpty(string)) { + return null == string ? null : EMPTY; + } + + final String str = string.toString(); + final int pos = isLastSeparator ? str.lastIndexOf(separator) : str.indexOf(separator); + if (INDEX_NOT_FOUND == pos) { + return str; + } + if (0 == pos) { + return EMPTY; + } + return str.substring(0, pos); + } + + /** + * 截取分隔字符串之后的字符串,不包括分隔字符串
+ * 如果给定的字符串为空串(null或""),返回原字符串
+ * 如果分隔字符串为空串(null或""),则返回空串,如果分隔字符串未找到,返回空串,举例如下: + * + *
+	 * StrUtil.subAfter(null, *, false)      = null
+	 * StrUtil.subAfter("", *, false)        = ""
+	 * StrUtil.subAfter(*, null, false)      = ""
+	 * StrUtil.subAfter("abc", "a", false)   = "bc"
+	 * StrUtil.subAfter("abcba", "b", false) = "cba"
+	 * StrUtil.subAfter("abc", "c", false)   = ""
+	 * StrUtil.subAfter("abc", "d", false)   = ""
+	 * StrUtil.subAfter("abc", "", false)    = "abc"
+	 * 
+ * + * @param string 被查找的字符串 + * @param separator 分隔字符串(不包括) + * @param isLastSeparator 是否查找最后一个分隔字符串(多次出现分隔字符串时选取最后一个),true为选取最后一个 + * @return 切割后的字符串 + * @since 3.1.1 + */ + public static String subAfter(CharSequence string, CharSequence separator, boolean isLastSeparator) { + if (isEmpty(string)) { + return null == string ? null : EMPTY; + } + if (separator == null) { + return EMPTY; + } + final String str = string.toString(); + final String sep = separator.toString(); + final int pos = isLastSeparator ? str.lastIndexOf(sep) : str.indexOf(sep); + if (INDEX_NOT_FOUND == pos || (string.length() - 1) == pos) { + return EMPTY; + } + return str.substring(pos + separator.length()); + } + + /** + * 截取分隔字符串之后的字符串,不包括分隔字符串
+ * 如果给定的字符串为空串(null或""),返回原字符串
+ * 如果分隔字符串为空串(null或""),则返回空串,如果分隔字符串未找到,返回空串,举例如下: + * + *
+	 * StrUtil.subAfter(null, *, false)      = null
+	 * StrUtil.subAfter("", *, false)        = ""
+	 * StrUtil.subAfter("abc", 'a', false)   = "bc"
+	 * StrUtil.subAfter("abcba", 'b', false) = "cba"
+	 * StrUtil.subAfter("abc", 'c', false)   = ""
+	 * StrUtil.subAfter("abc", 'd', false)   = ""
+	 * 
+ * + * @param string 被查找的字符串 + * @param separator 分隔字符串(不包括) + * @param isLastSeparator 是否查找最后一个分隔字符串(多次出现分隔字符串时选取最后一个),true为选取最后一个 + * @return 切割后的字符串 + * @since 4.1.15 + */ + public static String subAfter(CharSequence string, char separator, boolean isLastSeparator) { + if (isEmpty(string)) { + return null == string ? null : EMPTY; + } + final String str = string.toString(); + final int pos = isLastSeparator ? str.lastIndexOf(separator) : str.indexOf(separator); + if (INDEX_NOT_FOUND == pos) { + return EMPTY; + } + return str.substring(pos + 1); + } + + /** + * 截取指定字符串中间部分,不包括标识字符串
+ *

+ * 栗子: + * + *

+	 * StrUtil.subBetween("wx[b]yz", "[", "]") = "b"
+	 * StrUtil.subBetween(null, *, *)          = null
+	 * StrUtil.subBetween(*, null, *)          = null
+	 * StrUtil.subBetween(*, *, null)          = null
+	 * StrUtil.subBetween("", "", "")          = ""
+	 * StrUtil.subBetween("", "", "]")         = null
+	 * StrUtil.subBetween("", "[", "]")        = null
+	 * StrUtil.subBetween("yabcz", "", "")     = ""
+	 * StrUtil.subBetween("yabcz", "y", "z")   = "abc"
+	 * StrUtil.subBetween("yabczyabcz", "y", "z")   = "abc"
+	 * 
+ * + * @param str 被切割的字符串 + * @param before 截取开始的字符串标识 + * @param after 截取到的字符串标识 + * @return 截取后的字符串 + * @since 3.1.1 + */ + public static String subBetween(CharSequence str, CharSequence before, CharSequence after) { + if (str == null || before == null || after == null) { + return null; + } + + final String str2 = str.toString(); + final String before2 = before.toString(); + final String after2 = after.toString(); + + final int start = str2.indexOf(before2); + if (start != INDEX_NOT_FOUND) { + final int end = str2.indexOf(after2, start + before2.length()); + if (end != INDEX_NOT_FOUND) { + return str2.substring(start + before2.length(), end); + } + } + return null; + } + + /** + * 截取指定字符串中间部分,不包括标识字符串
+ *

+ * 栗子: + * + *

+	 * StrUtil.subBetween(null, *)            = null
+	 * StrUtil.subBetween("", "")             = ""
+	 * StrUtil.subBetween("", "tag")          = null
+	 * StrUtil.subBetween("tagabctag", null)  = null
+	 * StrUtil.subBetween("tagabctag", "")    = ""
+	 * StrUtil.subBetween("tagabctag", "tag") = "abc"
+	 * 
+ * + * @param str 被切割的字符串 + * @param beforeAndAfter 截取开始和结束的字符串标识 + * @return 截取后的字符串 + * @since 3.1.1 + */ + public static String subBetween(CharSequence str, CharSequence beforeAndAfter) { + return subBetween(str, beforeAndAfter, beforeAndAfter); + } + + /** + * 截取指定字符串多段中间部分,不包括标识字符串
+ *

+ * 栗子: + * + *

+	 * StrUtil.subBetweenAll("wx[b]y[z]", "[", "]") 		= ["b","z"]
+	 * StrUtil.subBetweenAll(null, *, *)          			= []
+	 * StrUtil.subBetweenAll(*, null, *)          			= []
+	 * StrUtil.subBetweenAll(*, *, null)          			= []
+	 * StrUtil.subBetweenAll("", "", "")          			= []
+	 * StrUtil.subBetweenAll("", "", "]")         			= []
+	 * StrUtil.subBetweenAll("", "[", "]")        			= []
+	 * StrUtil.subBetweenAll("yabcz", "", "")     			= []
+	 * StrUtil.subBetweenAll("yabcz", "y", "z")   			= ["abc"]
+	 * StrUtil.subBetweenAll("yabczyabcz", "y", "z")   		= ["abc","abc"]
+	 * StrUtil.subBetweenAll("[yabc[zy]abcz]", "[", "]");   = ["zy"]           重叠时只截取内部,
+	 * 
+ * + * @param str 被切割的字符串 + * @param prefix 截取开始的字符串标识 + * @param suffix 截取到的字符串标识 + * @return 截取后的字符串 + * @author dahuoyzs + * @since 5.2.5 + */ + public static String[] subBetweenAll(CharSequence str, CharSequence prefix, CharSequence suffix) { + if (hasEmpty(str, prefix, suffix) || + // 不包含起始字符串,则肯定没有子串 + !contains(str, prefix)) { + return new String[0]; + } + + final List result = new LinkedList<>(); + final String[] split = splitToArray(str, prefix); + if (prefix.equals(suffix)) { + // 前后缀字符相同,单独处理 + for (int i = 1, length = split.length - 1; i < length; i += 2) { + result.add(split[i]); + } + } else { + int suffixIndex; + String fragment; + for (int i = 1; i < split.length; i++) { + fragment = split[i]; + suffixIndex = fragment.indexOf(suffix.toString()); + if (suffixIndex > 0) { + result.add(fragment.substring(0, suffixIndex)); + } + } + } + + return result.toArray(new String[0]); + } + + /** + * 截取指定字符串多段中间部分,不包括标识字符串
+ *

+ * 栗子: + * + *

+	 * StrUtil.subBetweenAll(null, *)          			= []
+	 * StrUtil.subBetweenAll(*, null)          			= []
+	 * StrUtil.subBetweenAll(*, *)          			= []
+	 * StrUtil.subBetweenAll("", "")          			= []
+	 * StrUtil.subBetweenAll("", "#")         			= []
+	 * StrUtil.subBetweenAll("gotanks", "")     		= []
+	 * StrUtil.subBetweenAll("#gotanks#", "#")   		= ["gotanks"]
+	 * StrUtil.subBetweenAll("#hello# #world#!", "#")   = ["hello", "world"]
+	 * StrUtil.subBetweenAll("#hello# world#!", "#");   = ["hello"]
+	 * 
+ * + * @param str 被切割的字符串 + * @param prefixAndSuffix 截取开始和结束的字符串标识 + * @return 截取后的字符串 + * @author gotanks + * @since 5.5.0 + */ + public static String[] subBetweenAll(CharSequence str, CharSequence prefixAndSuffix) { + return subBetweenAll(str, prefixAndSuffix, prefixAndSuffix); + } + + // ------------------------------------------------------------------------ repeat + + /** + * 重复某个字符 + * + *
+	 * StrUtil.repeat('e', 0)  = ""
+	 * StrUtil.repeat('e', 3)  = "eee"
+	 * StrUtil.repeat('e', -2) = ""
+	 * 
+ * + * @param c 被重复的字符 + * @param count 重复的数目,如果小于等于0则返回"" + * @return 重复字符字符串 + */ + public static String repeat(char c, int count) { + if (count <= 0) { + return EMPTY; + } + + char[] result = new char[count]; + for (int i = 0; i < count; i++) { + result[i] = c; + } + return new String(result); + } + + /** + * 重复某个字符串 + * + * @param str 被重复的字符 + * @param count 重复的数目 + * @return 重复字符字符串 + */ + public static String repeat(CharSequence str, int count) { + if (null == str) { + return null; + } + if (count <= 0 || str.length() == 0) { + return EMPTY; + } + if (count == 1) { + return str.toString(); + } + + // 检查 + final int len = str.length(); + final long longSize = (long) len * (long) count; + final int size = (int) longSize; + if (size != longSize) { + throw new ArrayIndexOutOfBoundsException("Required String length is too large: " + longSize); + } + + final char[] array = new char[size]; + str.toString().getChars(0, len, array, 0); + int n; + for (n = len; n < size - n; n <<= 1) {// n <<= 1相当于n *2 + System.arraycopy(array, 0, array, n, n); + } + System.arraycopy(array, 0, array, n, size - n); + return new String(array); + } + + /** + * 重复某个字符串到指定长度 + * + * @param str 被重复的字符 + * @param padLen 指定长度 + * @return 重复字符字符串 + * @since 4.3.2 + */ + public static String repeatByLength(CharSequence str, int padLen) { + if (null == str) { + return null; + } + if (padLen <= 0) { + return StrUtil.EMPTY; + } + final int strLen = str.length(); + if (strLen == padLen) { + return str.toString(); + } else if (strLen > padLen) { + return subPre(str, padLen); + } + + // 重复,直到达到指定长度 + final char[] padding = new char[padLen]; + for (int i = 0; i < padLen; i++) { + padding[i] = str.charAt(i % strLen); + } + return new String(padding); + } + + /** + * 重复某个字符串并通过分界符连接 + * + *
+	 * StrUtil.repeatAndJoin("?", 5, ",")   = "?,?,?,?,?"
+	 * StrUtil.repeatAndJoin("?", 0, ",")   = ""
+	 * StrUtil.repeatAndJoin("?", 5, null) = "?????"
+	 * 
+ * + * @param str 被重复的字符串 + * @param count 数量 + * @param delimiter 分界符 + * @return 连接后的字符串 + * @since 4.0.1 + */ + public static String repeatAndJoin(CharSequence str, int count, CharSequence delimiter) { + if (count <= 0) { + return EMPTY; + } + final StringBuilder builder = new StringBuilder(str.length() * count); + builder.append(str); + count--; + + final boolean isAppendDelimiter = isNotEmpty(delimiter); + while (count-- > 0) { + if (isAppendDelimiter) { + builder.append(delimiter); + } + builder.append(str); + } + return builder.toString(); + } + + // ------------------------------------------------------------------------ equals + + /** + * 比较两个字符串(大小写敏感)。 + * + *
+	 * equals(null, null)   = true
+	 * equals(null, "abc")  = false
+	 * equals("abc", null)  = false
+	 * equals("abc", "abc") = true
+	 * equals("abc", "ABC") = false
+	 * 
+ * + * @param str1 要比较的字符串1 + * @param str2 要比较的字符串2 + * @return 如果两个字符串相同,或者都是{@code null},则返回{@code true} + */ + public static boolean equals(CharSequence str1, CharSequence str2) { + return equals(str1, str2, false); + } + + /** + * 比较两个字符串(大小写不敏感)。 + * + *
+	 * equalsIgnoreCase(null, null)   = true
+	 * equalsIgnoreCase(null, "abc")  = false
+	 * equalsIgnoreCase("abc", null)  = false
+	 * equalsIgnoreCase("abc", "abc") = true
+	 * equalsIgnoreCase("abc", "ABC") = true
+	 * 
+ * + * @param str1 要比较的字符串1 + * @param str2 要比较的字符串2 + * @return 如果两个字符串相同,或者都是{@code null},则返回{@code true} + */ + public static boolean equalsIgnoreCase(CharSequence str1, CharSequence str2) { + return equals(str1, str2, true); + } + + /** + * 比较两个字符串是否相等,规则如下 + *
    + *
  • str1和str2都为{@code null}
  • + *
  • 忽略大小写使用{@link String#equalsIgnoreCase(String)}判断相等
  • + *
  • 不忽略大小写使用{@link String#contentEquals(CharSequence)}判断相等
  • + *
+ * + * @param str1 要比较的字符串1 + * @param str2 要比较的字符串2 + * @param ignoreCase 是否忽略大小写 + * @return 如果两个字符串相同,或者都是{@code null},则返回{@code true} + * @since 3.2.0 + */ + public static boolean equals(CharSequence str1, CharSequence str2, boolean ignoreCase) { + if (null == str1) { + // 只有两个都为null才判断相等 + return str2 == null; + } + if (null == str2) { + // 字符串2空,字符串1非空,直接false + return false; + } + + if (ignoreCase) { + return str1.toString().equalsIgnoreCase(str2.toString()); + } else { + return str1.toString().contentEquals(str2); + } + } + + /** + * 给定字符串是否与提供的中任一字符串相同(忽略大小写),相同则返回{@code true},没有相同的返回{@code false}
+ * 如果参与比对的字符串列表为空,返回{@code false} + * + * @param str1 给定需要检查的字符串 + * @param strs 需要参与比对的字符串列表 + * @return 是否相同 + * @since 4.3.2 + */ + public static boolean equalsAnyIgnoreCase(CharSequence str1, CharSequence... strs) { + return equalsAny(str1, true, strs); + } + + /** + * 给定字符串是否与提供的中任一字符串相同,相同则返回{@code true},没有相同的返回{@code false}
+ * 如果参与比对的字符串列表为空,返回{@code false} + * + * @param str1 给定需要检查的字符串 + * @param strs 需要参与比对的字符串列表 + * @return 是否相同 + * @since 4.3.2 + */ + public static boolean equalsAny(CharSequence str1, CharSequence... strs) { + return equalsAny(str1, false, strs); + } + + /** + * 给定字符串是否与提供的中任一字符串相同,相同则返回{@code true},没有相同的返回{@code false}
+ * 如果参与比对的字符串列表为空,返回{@code false} + * + * @param str1 给定需要检查的字符串 + * @param ignoreCase 是否忽略大小写 + * @param strs 需要参与比对的字符串列表 + * @return 是否相同 + * @since 4.3.2 + */ + public static boolean equalsAny(CharSequence str1, boolean ignoreCase, CharSequence... strs) { + if (ArrayUtil.isEmpty(strs)) { + return false; + } + + for (CharSequence str : strs) { + if (equals(str1, str, ignoreCase)) { + return true; + } + } + return false; + } + + /** + * 字符串指定位置的字符是否与给定字符相同
+ * 如果字符串为null,返回false
+ * 如果给定的位置大于字符串长度,返回false
+ * 如果给定的位置小于0,返回false + * + * @param str 字符串 + * @param position 位置 + * @param c 需要对比的字符 + * @return 字符串指定位置的字符是否与给定字符相同 + * @since 3.3.1 + */ + public static boolean equalsCharAt(CharSequence str, int position, char c) { + if (null == str || position < 0) { + return false; + } + return str.length() > position && c == str.charAt(position); + } + + /** + * 截取第一个字串的部分字符,与第二个字符串比较(长度一致),判断截取的子串是否相同
+ * 任意一个字符串为null返回false + * + * @param str1 第一个字符串 + * @param start1 第一个字符串开始的位置 + * @param str2 第二个字符串 + * @param ignoreCase 是否忽略大小写 + * @return 子串是否相同 + * @since 3.2.1 + */ + public static boolean isSubEquals(CharSequence str1, int start1, CharSequence str2, boolean ignoreCase) { + return isSubEquals(str1, start1, str2, 0, str2.length(), ignoreCase); + } + + /** + * 截取两个字符串的不同部分(长度一致),判断截取的子串是否相同
+ * 任意一个字符串为null返回false + * + * @param str1 第一个字符串 + * @param start1 第一个字符串开始的位置 + * @param str2 第二个字符串 + * @param start2 第二个字符串开始的位置 + * @param length 截取长度 + * @param ignoreCase 是否忽略大小写 + * @return 子串是否相同 + * @since 3.2.1 + */ + public static boolean isSubEquals(CharSequence str1, int start1, CharSequence str2, int start2, int length, boolean ignoreCase) { + if (null == str1 || null == str2) { + return false; + } + + return str1.toString().regionMatches(ignoreCase, start1, str2.toString(), start2, length); + } + + // ------------------------------------------------------------------------ format + + /** + * 格式化文本, {} 表示占位符
+ * 此方法只是简单将占位符 {} 按照顺序替换为参数
+ * 如果想输出 {} 使用 \\转义 { 即可,如果想输出 {} 之前的 \ 使用双转义符 \\\\ 即可
+ * 例:
+ * 通常使用:format("this is {} for {}", "a", "b") =》 this is a for b
+ * 转义{}: format("this is \\{} for {}", "a", "b") =》 this is {} for a
+ * 转义\: format("this is \\\\{} for {}", "a", "b") =》 this is \a for b
+ * + * @param template 文本模板,被替换的部分用 {} 表示,如果模板为null,返回"null" + * @param params 参数值 + * @return 格式化后的文本,如果模板为null,返回"null" + */ + public static String format(CharSequence template, Object... params) { + if (null == template) { + return NULL; + } + if (ArrayUtil.isEmpty(params) || isBlank(template)) { + return template.toString(); + } + return StrFormatter.format(template.toString(), params); + } + + /** + * 有序的格式化文本,使用{number}做为占位符
+ * 通常使用:format("this is {0} for {1}", "a", "b") =》 this is a for b
+ * + * @param pattern 文本格式 + * @param arguments 参数 + * @return 格式化后的文本 + */ + public static String indexedFormat(CharSequence pattern, Object... arguments) { + return MessageFormat.format(pattern.toString(), arguments); + } + // ------------------------------------------------------------------------ bytes + + /** + * 编码字符串,编码为UTF-8 + * + * @param str 字符串 + * @return 编码后的字节码 + */ + public static byte[] utf8Bytes(CharSequence str) { + return bytes(str, CharsetUtil.CHARSET_UTF_8); + } + + /** + * 编码字符串
+ * 使用系统默认编码 + * + * @param str 字符串 + * @return 编码后的字节码 + */ + public static byte[] bytes(CharSequence str) { + return bytes(str, Charset.defaultCharset()); + } + + /** + * 编码字符串 + * + * @param str 字符串 + * @param charset 字符集,如果此字段为空,则解码的结果取决于平台 + * @return 编码后的字节码 + */ + public static byte[] bytes(CharSequence str, String charset) { + return bytes(str, isBlank(charset) ? Charset.defaultCharset() : Charset.forName(charset)); + } + + /** + * 编码字符串 + * + * @param str 字符串 + * @param charset 字符集,如果此字段为空,则解码的结果取决于平台 + * @return 编码后的字节码 + */ + public static byte[] bytes(CharSequence str, Charset charset) { + if (str == null) { + return null; + } + + if (null == charset) { + return str.toString().getBytes(); + } + return str.toString().getBytes(charset); + } + + /** + * 字符串转换为byteBuffer + * + * @param str 字符串 + * @param charset 编码 + * @return byteBuffer + */ + public static ByteBuffer byteBuffer(CharSequence str, String charset) { + return ByteBuffer.wrap(bytes(str, charset)); + } + + // ------------------------------------------------------------------------ wrap + + /** + * 包装指定字符串
+ * 当前缀和后缀一致时使用此方法 + * + * @param str 被包装的字符串 + * @param prefixAndSuffix 前缀和后缀 + * @return 包装后的字符串 + * @since 3.1.0 + */ + public static String wrap(CharSequence str, CharSequence prefixAndSuffix) { + return wrap(str, prefixAndSuffix, prefixAndSuffix); + } + + /** + * 包装指定字符串 + * + * @param str 被包装的字符串 + * @param prefix 前缀 + * @param suffix 后缀 + * @return 包装后的字符串 + */ + public static String wrap(CharSequence str, CharSequence prefix, CharSequence suffix) { + return nullToEmpty(prefix).concat(nullToEmpty(str)).concat(nullToEmpty(suffix)); + } + + /** + * 使用单个字符包装多个字符串 + * + * @param prefixAndSuffix 前缀和后缀 + * @param strs 多个字符串 + * @return 包装的字符串数组 + * @since 5.4.1 + */ + public static String[] wrapAllWithPair(CharSequence prefixAndSuffix, CharSequence... strs) { + return wrapAll(prefixAndSuffix, prefixAndSuffix, strs); + } + + /** + * 包装多个字符串 + * + * @param prefix 前缀 + * @param suffix 后缀 + * @param strs 多个字符串 + * @return 包装的字符串数组 + * @since 4.0.7 + */ + public static String[] wrapAll(CharSequence prefix, CharSequence suffix, CharSequence... strs) { + final String[] results = new String[strs.length]; + for (int i = 0; i < strs.length; i++) { + results[i] = wrap(strs[i], prefix, suffix); + } + return results; + } + + /** + * 包装指定字符串,如果前缀或后缀已经包含对应的字符串,则不再包装 + * + * @param str 被包装的字符串 + * @param prefix 前缀 + * @param suffix 后缀 + * @return 包装后的字符串 + */ + public static String wrapIfMissing(CharSequence str, CharSequence prefix, CharSequence suffix) { + int len = 0; + if (isNotEmpty(str)) { + len += str.length(); + } + if (isNotEmpty(prefix)) { + len += prefix.length(); + } + if (isNotEmpty(suffix)) { + len += suffix.length(); + } + StringBuilder sb = new StringBuilder(len); + if (isNotEmpty(prefix) && !startWith(str, prefix)) { + sb.append(prefix); + } + if (isNotEmpty(str)) { + sb.append(str); + } + if (isNotEmpty(suffix) && !endWith(str, suffix)) { + sb.append(suffix); + } + return sb.toString(); + } + + /** + * 使用成对的字符包装多个字符串,如果已经包装,则不再包装 + * + * @param prefixAndSuffix 前缀和后缀 + * @param strs 多个字符串 + * @return 包装的字符串数组 + * @since 5.4.1 + */ + public static String[] wrapAllWithPairIfMissing(CharSequence prefixAndSuffix, CharSequence... strs) { + return wrapAllIfMissing(prefixAndSuffix, prefixAndSuffix, strs); + } + + /** + * 包装多个字符串,如果已经包装,则不再包装 + * + * @param prefix 前缀 + * @param suffix 后缀 + * @param strs 多个字符串 + * @return 包装的字符串数组 + * @since 4.0.7 + */ + public static String[] wrapAllIfMissing(CharSequence prefix, CharSequence suffix, CharSequence... strs) { + final String[] results = new String[strs.length]; + for (int i = 0; i < strs.length; i++) { + results[i] = wrapIfMissing(strs[i], prefix, suffix); + } + return results; + } + + /** + * 去掉字符包装,如果未被包装则返回原字符串 + * + * @param str 字符串 + * @param prefix 前置字符串 + * @param suffix 后置字符串 + * @return 去掉包装字符的字符串 + * @since 4.0.1 + */ + public static String unWrap(CharSequence str, String prefix, String suffix) { + if (isWrap(str, prefix, suffix)) { + return sub(str, prefix.length(), str.length() - suffix.length()); + } + return str.toString(); + } + + /** + * 去掉字符包装,如果未被包装则返回原字符串 + * + * @param str 字符串 + * @param prefix 前置字符 + * @param suffix 后置字符 + * @return 去掉包装字符的字符串 + * @since 4.0.1 + */ + public static String unWrap(CharSequence str, char prefix, char suffix) { + if (isEmpty(str)) { + return str(str); + } + if (str.charAt(0) == prefix && str.charAt(str.length() - 1) == suffix) { + return sub(str, 1, str.length() - 1); + } + return str.toString(); + } + + /** + * 去掉字符包装,如果未被包装则返回原字符串 + * + * @param str 字符串 + * @param prefixAndSuffix 前置和后置字符 + * @return 去掉包装字符的字符串 + * @since 4.0.1 + */ + public static String unWrap(CharSequence str, char prefixAndSuffix) { + return unWrap(str, prefixAndSuffix, prefixAndSuffix); + } + + /** + * 指定字符串是否被包装 + * + * @param str 字符串 + * @param prefix 前缀 + * @param suffix 后缀 + * @return 是否被包装 + */ + public static boolean isWrap(CharSequence str, String prefix, String suffix) { + if (ArrayUtil.hasNull(str, prefix, suffix)) { + return false; + } + final String str2 = str.toString(); + return str2.startsWith(prefix) && str2.endsWith(suffix); + } + + /** + * 指定字符串是否被同一字符包装(前后都有这些字符串) + * + * @param str 字符串 + * @param wrapper 包装字符串 + * @return 是否被包装 + */ + public static boolean isWrap(CharSequence str, String wrapper) { + return isWrap(str, wrapper, wrapper); + } + + /** + * 指定字符串是否被同一字符包装(前后都有这些字符串) + * + * @param str 字符串 + * @param wrapper 包装字符 + * @return 是否被包装 + */ + public static boolean isWrap(CharSequence str, char wrapper) { + return isWrap(str, wrapper, wrapper); + } + + /** + * 指定字符串是否被包装 + * + * @param str 字符串 + * @param prefixChar 前缀 + * @param suffixChar 后缀 + * @return 是否被包装 + */ + public static boolean isWrap(CharSequence str, char prefixChar, char suffixChar) { + if (null == str) { + return false; + } + + return str.charAt(0) == prefixChar && str.charAt(str.length() - 1) == suffixChar; + } + + // ------------------------------------------------------------------------ pad + + /** + * 补充字符串以满足指定长度,如果提供的字符串大于指定长度,截断之 + * 同:leftPad (org.apache.commons.lang3.leftPad) + * + *
+	 * StrUtil.padPre(null, *, *);//null
+	 * StrUtil.padPre("1", 3, "ABC");//"AB1"
+	 * StrUtil.padPre("123", 2, "ABC");//"12"
+	 * StrUtil.padPre("1039", -1, "0");//"103"
+	 * 
+ * + * @param str 字符串 + * @param length 长度 + * @param padStr 补充的字符 + * @return 补充后的字符串 + */ + public static String padPre(CharSequence str, int length, CharSequence padStr) { + if (null == str) { + return null; + } + final int strLen = str.length(); + if (strLen == length) { + return str.toString(); + } else if (strLen > length) { + //如果提供的字符串大于指定长度,截断之 + return subPre(str, length); + } + + return repeatByLength(padStr, length - strLen).concat(str.toString()); + } + + /** + * 补充字符串以满足最小长度,如果提供的字符串大于指定长度,截断之 + * 同:leftPad (org.apache.commons.lang3.leftPad) + * + *
+	 * StrUtil.padPre(null, *, *);//null
+	 * StrUtil.padPre("1", 3, '0');//"001"
+	 * StrUtil.padPre("123", 2, '0');//"12"
+	 * 
+ * + * @param str 字符串 + * @param length 长度 + * @param padChar 补充的字符 + * @return 补充后的字符串 + */ + public static String padPre(CharSequence str, int length, char padChar) { + if (null == str) { + return null; + } + final int strLen = str.length(); + if (strLen == length) { + return str.toString(); + } else if (strLen > length) { + //如果提供的字符串大于指定长度,截断之 + return subPre(str, length); + } + + return repeat(padChar, length - strLen).concat(str.toString()); + } + + /** + * 补充字符串以满足最小长度,如果提供的字符串大于指定长度,截断之 + * + *
+	 * StrUtil.padAfter(null, *, *);//null
+	 * StrUtil.padAfter("1", 3, '0');//"100"
+	 * StrUtil.padAfter("123", 2, '0');//"23"
+	 * StrUtil.padAfter("123", -1, '0')//"" 空串
+	 * 
+ * + * @param str 字符串,如果为{@code null},直接返回null + * @param length 长度 + * @param padChar 补充的字符 + * @return 补充后的字符串 + */ + public static String padAfter(CharSequence str, int length, char padChar) { + if (null == str) { + return null; + } + final int strLen = str.length(); + if (strLen == length) { + return str.toString(); + } else if (strLen > length) { + //如果提供的字符串大于指定长度,截断之 + return sub(str, strLen - length, strLen); + } + + return str.toString().concat(repeat(padChar, length - strLen)); + } + + /** + * 补充字符串以满足最小长度 + * + *
+	 * StrUtil.padAfter(null, *, *);//null
+	 * StrUtil.padAfter("1", 3, "ABC");//"1AB"
+	 * StrUtil.padAfter("123", 2, "ABC");//"23"
+	 * 
+ * + * @param str 字符串,如果为{@code null},直接返回null + * @param length 长度 + * @param padStr 补充的字符 + * @return 补充后的字符串 + * @since 4.3.2 + */ + public static String padAfter(CharSequence str, int length, CharSequence padStr) { + if (null == str) { + return null; + } + final int strLen = str.length(); + if (strLen == length) { + return str.toString(); + } else if (strLen > length) { + //如果提供的字符串大于指定长度,截断之 + return subSufByLength(str, length); + } + + return str.toString().concat(repeatByLength(padStr, length - strLen)); + } + + // ------------------------------------------------------------------------ center + + /** + * 居中字符串,两边补充指定字符串,如果指定长度小于字符串,则返回原字符串 + * + *
+	 * StrUtil.center(null, *)   = null
+	 * StrUtil.center("", 4)     = "    "
+	 * StrUtil.center("ab", -1)  = "ab"
+	 * StrUtil.center("ab", 4)   = " ab "
+	 * StrUtil.center("abcd", 2) = "abcd"
+	 * StrUtil.center("a", 4)    = " a  "
+	 * 
+ * + * @param str 字符串 + * @param size 指定长度 + * @return 补充后的字符串 + * @since 4.3.2 + */ + public static String center(CharSequence str, final int size) { + return center(str, size, CharUtil.SPACE); + } + + /** + * 居中字符串,两边补充指定字符串,如果指定长度小于字符串,则返回原字符串 + * + *
+	 * StrUtil.center(null, *, *)     = null
+	 * StrUtil.center("", 4, ' ')     = "    "
+	 * StrUtil.center("ab", -1, ' ')  = "ab"
+	 * StrUtil.center("ab", 4, ' ')   = " ab "
+	 * StrUtil.center("abcd", 2, ' ') = "abcd"
+	 * StrUtil.center("a", 4, ' ')    = " a  "
+	 * StrUtil.center("a", 4, 'y')   = "yayy"
+	 * StrUtil.center("abc", 7, ' ')   = "  abc  "
+	 * 
+ * + * @param str 字符串 + * @param size 指定长度 + * @param padChar 两边补充的字符 + * @return 补充后的字符串 + * @since 4.3.2 + */ + public static String center(CharSequence str, final int size, char padChar) { + if (str == null || size <= 0) { + return str(str); + } + final int strLen = str.length(); + final int pads = size - strLen; + if (pads <= 0) { + return str.toString(); + } + str = padPre(str, strLen + pads / 2, padChar); + str = padAfter(str, size, padChar); + return str.toString(); + } + + /** + * 居中字符串,两边补充指定字符串,如果指定长度小于字符串,则返回原字符串 + * + *
+	 * StrUtil.center(null, *, *)     = null
+	 * StrUtil.center("", 4, " ")     = "    "
+	 * StrUtil.center("ab", -1, " ")  = "ab"
+	 * StrUtil.center("ab", 4, " ")   = " ab "
+	 * StrUtil.center("abcd", 2, " ") = "abcd"
+	 * StrUtil.center("a", 4, " ")    = " a  "
+	 * StrUtil.center("a", 4, "yz")   = "yayz"
+	 * StrUtil.center("abc", 7, null) = "  abc  "
+	 * StrUtil.center("abc", 7, "")   = "  abc  "
+	 * 
+ * + * @param str 字符串 + * @param size 指定长度 + * @param padStr 两边补充的字符串 + * @return 补充后的字符串 + */ + public static String center(CharSequence str, final int size, CharSequence padStr) { + if (str == null || size <= 0) { + return str(str); + } + if (isEmpty(padStr)) { + padStr = SPACE; + } + final int strLen = str.length(); + final int pads = size - strLen; + if (pads <= 0) { + return str.toString(); + } + str = padPre(str, strLen + pads / 2, padStr); + str = padAfter(str, size, padStr); + return str.toString(); + } + + // ------------------------------------------------------------------------ str + + /** + * {@link CharSequence} 转为字符串,null安全 + * + * @param cs {@link CharSequence} + * @return 字符串 + */ + public static String str(CharSequence cs) { + return null == cs ? null : cs.toString(); + } + + // ------------------------------------------------------------------------ count + + /** + * 统计指定内容中包含指定字符串的数量
+ * 参数为 {@code null} 或者 "" 返回 {@code 0}. + * + *
+	 * StrUtil.count(null, *)       = 0
+	 * StrUtil.count("", *)         = 0
+	 * StrUtil.count("abba", null)  = 0
+	 * StrUtil.count("abba", "")    = 0
+	 * StrUtil.count("abba", "a")   = 2
+	 * StrUtil.count("abba", "ab")  = 1
+	 * StrUtil.count("abba", "xxx") = 0
+	 * 
+ * + * @param content 被查找的字符串 + * @param strForSearch 需要查找的字符串 + * @return 查找到的个数 + */ + public static int count(CharSequence content, CharSequence strForSearch) { + if (hasEmpty(content, strForSearch) || strForSearch.length() > content.length()) { + return 0; + } + + int count = 0; + int idx = 0; + final String content2 = content.toString(); + final String strForSearch2 = strForSearch.toString(); + while ((idx = content2.indexOf(strForSearch2, idx)) > -1) { + count++; + idx += strForSearch.length(); + } + return count; + } + + /** + * 统计指定内容中包含指定字符的数量 + * + * @param content 内容 + * @param charForSearch 被统计的字符 + * @return 包含数量 + */ + public static int count(CharSequence content, char charForSearch) { + int count = 0; + if (isEmpty(content)) { + return 0; + } + int contentLength = content.length(); + for (int i = 0; i < contentLength; i++) { + if (charForSearch == content.charAt(i)) { + count++; + } + } + return count; + } + + // ------------------------------------------------------------------------ compare + + /** + * 比较两个字符串,用于排序 + * + *
+	 * StrUtil.compare(null, null, *)     = 0
+	 * StrUtil.compare(null , "a", true)  < 0
+	 * StrUtil.compare(null , "a", false) > 0
+	 * StrUtil.compare("a", null, true)   > 0
+	 * StrUtil.compare("a", null, false)  < 0
+	 * StrUtil.compare("abc", "abc", *)   = 0
+	 * StrUtil.compare("a", "b", *)       < 0
+	 * StrUtil.compare("b", "a", *)       > 0
+	 * StrUtil.compare("a", "B", *)       > 0
+	 * StrUtil.compare("ab", "abc", *)    < 0
+	 * 
+ * + * @param str1 字符串1 + * @param str2 字符串2 + * @param nullIsLess {@code null} 值是否排在前(null是否小于非空值) + * @return 排序值。负数:str1 < str2,正数:str1 > str2, 0:str1 == str2 + */ + public static int compare(final CharSequence str1, final CharSequence str2, final boolean nullIsLess) { + if (str1 == str2) { + return 0; + } + if (str1 == null) { + return nullIsLess ? -1 : 1; + } + if (str2 == null) { + return nullIsLess ? 1 : -1; + } + return str1.toString().compareTo(str2.toString()); + } + + /** + * 比较两个字符串,用于排序,大小写不敏感 + * + *
+	 * StrUtil.compareIgnoreCase(null, null, *)     = 0
+	 * StrUtil.compareIgnoreCase(null , "a", true)  < 0
+	 * StrUtil.compareIgnoreCase(null , "a", false) > 0
+	 * StrUtil.compareIgnoreCase("a", null, true)   > 0
+	 * StrUtil.compareIgnoreCase("a", null, false)  < 0
+	 * StrUtil.compareIgnoreCase("abc", "abc", *)   = 0
+	 * StrUtil.compareIgnoreCase("abc", "ABC", *)   = 0
+	 * StrUtil.compareIgnoreCase("a", "b", *)       < 0
+	 * StrUtil.compareIgnoreCase("b", "a", *)       > 0
+	 * StrUtil.compareIgnoreCase("a", "B", *)       < 0
+	 * StrUtil.compareIgnoreCase("A", "b", *)       < 0
+	 * StrUtil.compareIgnoreCase("ab", "abc", *)    < 0
+	 * 
+ * + * @param str1 字符串1 + * @param str2 字符串2 + * @param nullIsLess {@code null} 值是否排在前(null是否小于非空值) + * @return 排序值。负数:str1 < str2,正数:str1 > str2, 0:str1 == str2 + */ + public static int compareIgnoreCase(CharSequence str1, CharSequence str2, boolean nullIsLess) { + if (str1 == str2) { + return 0; + } + if (str1 == null) { + return nullIsLess ? -1 : 1; + } + if (str2 == null) { + return nullIsLess ? 1 : -1; + } + return str1.toString().compareToIgnoreCase(str2.toString()); + } + + /** + * 比较两个版本
+ * null版本排在最小:即: + * + *
+	 * StrUtil.compareVersion(null, "v1") < 0
+	 * StrUtil.compareVersion("v1", "v1")  = 0
+	 * StrUtil.compareVersion(null, null)   = 0
+	 * StrUtil.compareVersion("v1", null) > 0
+	 * StrUtil.compareVersion("1.0.0", "1.0.2") < 0
+	 * StrUtil.compareVersion("1.0.2", "1.0.2a") < 0
+	 * StrUtil.compareVersion("1.13.0", "1.12.1c") > 0
+	 * StrUtil.compareVersion("V0.0.20170102", "V0.0.20170101") > 0
+	 * 
+ * + * @param version1 版本1 + * @param version2 版本2 + * @return 排序值。负数:version1 < version2,正数:version1 > version2, 0:version1 == version2 + * @since 4.0.2 + */ + public static int compareVersion(CharSequence version1, CharSequence version2) { + return VersionComparator.INSTANCE.compare(str(version1), str(version2)); + } + + // ------------------------------------------------------------------------ append and prepend + + /** + * 如果给定字符串不是以给定的一个或多个字符串为结尾,则在尾部添加结尾字符串
+ * 不忽略大小写 + * + * @param str 被检查的字符串 + * @param suffix 需要添加到结尾的字符串 + * @param suffixes 需要额外检查的结尾字符串,如果以这些中的一个为结尾,则不再添加 + * @return 如果已经结尾,返回原字符串,否则返回添加结尾的字符串 + * @since 3.0.7 + */ + public static String appendIfMissing(CharSequence str, CharSequence suffix, CharSequence... suffixes) { + return appendIfMissing(str, suffix, false, suffixes); + } + + /** + * 如果给定字符串不是以给定的一个或多个字符串为结尾,则在尾部添加结尾字符串
+ * 忽略大小写 + * + * @param str 被检查的字符串 + * @param suffix 需要添加到结尾的字符串 + * @param suffixes 需要额外检查的结尾字符串,如果以这些中的一个为结尾,则不再添加 + * @return 如果已经结尾,返回原字符串,否则返回添加结尾的字符串 + * @since 3.0.7 + */ + public static String appendIfMissingIgnoreCase(CharSequence str, CharSequence suffix, CharSequence... suffixes) { + return appendIfMissing(str, suffix, true, suffixes); + } + + /** + * 如果给定字符串不是以给定的一个或多个字符串为结尾,则在尾部添加结尾字符串 + * + * @param str 被检查的字符串 + * @param suffix 需要添加到结尾的字符串,不参与检查匹配 + * @param ignoreCase 检查结尾时是否忽略大小写 + * @param testSuffixes 需要额外检查的结尾字符串,如果以这些中的一个为结尾,则不再添加 + * @return 如果已经结尾,返回原字符串,否则返回添加结尾的字符串 + * @since 3.0.7 + */ + public static String appendIfMissing(CharSequence str, CharSequence suffix, boolean ignoreCase, CharSequence... testSuffixes) { + if (str == null || isEmpty(suffix) || endWith(str, suffix, ignoreCase)) { + return str(str); + } + if (ArrayUtil.isNotEmpty(testSuffixes)) { + for (final CharSequence testSuffix : testSuffixes) { + if (endWith(str, testSuffix, ignoreCase)) { + return str.toString(); + } + } + } + return str.toString().concat(suffix.toString()); + } + + /** + * 如果给定字符串不是以给定的一个或多个字符串为开头,则在首部添加起始字符串
+ * 不忽略大小写 + * + * @param str 被检查的字符串 + * @param prefix 需要添加到首部的字符串 + * @param prefixes 需要额外检查的首部字符串,如果以这些中的一个为起始,则不再添加 + * @return 如果已经结尾,返回原字符串,否则返回添加结尾的字符串 + * @since 3.0.7 + */ + public static String prependIfMissing(CharSequence str, CharSequence prefix, CharSequence... prefixes) { + return prependIfMissing(str, prefix, false, prefixes); + } + + /** + * 如果给定字符串不是以给定的一个或多个字符串为开头,则在首部添加起始字符串
+ * 忽略大小写 + * + * @param str 被检查的字符串 + * @param prefix 需要添加到首部的字符串 + * @param prefixes 需要额外检查的首部字符串,如果以这些中的一个为起始,则不再添加 + * @return 如果已经结尾,返回原字符串,否则返回添加结尾的字符串 + * @since 3.0.7 + */ + public static String prependIfMissingIgnoreCase(CharSequence str, CharSequence prefix, CharSequence... prefixes) { + return prependIfMissing(str, prefix, true, prefixes); + } + + /** + * 如果给定字符串不是以给定的一个或多个字符串为开头,则在首部添加起始字符串 + * + * @param str 被检查的字符串 + * @param prefix 需要添加到首部的字符串 + * @param ignoreCase 检查结尾时是否忽略大小写 + * @param prefixes 需要额外检查的首部字符串,如果以这些中的一个为起始,则不再添加 + * @return 如果已经结尾,返回原字符串,否则返回添加结尾的字符串 + * @since 3.0.7 + */ + public static String prependIfMissing(CharSequence str, CharSequence prefix, boolean ignoreCase, CharSequence... prefixes) { + if (str == null || isEmpty(prefix) || startWith(str, prefix, ignoreCase)) { + return str(str); + } + if (prefixes != null) { + for (final CharSequence s : prefixes) { + if (startWith(str, s, ignoreCase)) { + return str.toString(); + } + } + } + return prefix.toString().concat(str.toString()); + } + + // ------------------------------------------------------------------------ replace + + /** + * 替换字符串中的指定字符串,忽略大小写 + * + * @param str 字符串 + * @param searchStr 被查找的字符串 + * @param replacement 被替换的字符串 + * @return 替换后的字符串 + * @since 4.0.3 + */ + public static String replaceIgnoreCase(CharSequence str, CharSequence searchStr, CharSequence replacement) { + return replace(str, 0, searchStr, replacement, true); + } + + /** + * 替换字符串中的指定字符串 + * + * @param str 字符串 + * @param searchStr 被查找的字符串 + * @param replacement 被替换的字符串 + * @return 替换后的字符串 + * @since 4.0.3 + */ + public static String replace(CharSequence str, CharSequence searchStr, CharSequence replacement) { + return replace(str, 0, searchStr, replacement, false); + } + + /** + * 替换字符串中的指定字符串 + * + * @param str 字符串 + * @param searchStr 被查找的字符串 + * @param replacement 被替换的字符串 + * @param ignoreCase 是否忽略大小写 + * @return 替换后的字符串 + * @since 4.0.3 + */ + public static String replace(CharSequence str, CharSequence searchStr, CharSequence replacement, boolean ignoreCase) { + return replace(str, 0, searchStr, replacement, ignoreCase); + } + + /** + * 替换字符串中的指定字符串 + * + * @param str 字符串 + * @param fromIndex 开始位置(包括) + * @param searchStr 被查找的字符串 + * @param replacement 被替换的字符串 + * @param ignoreCase 是否忽略大小写 + * @return 替换后的字符串 + * @since 4.0.3 + */ + public static String replace(CharSequence str, int fromIndex, CharSequence searchStr, CharSequence replacement, boolean ignoreCase) { + if (isEmpty(str) || isEmpty(searchStr)) { + return str(str); + } + if (null == replacement) { + replacement = EMPTY; + } + + final int strLength = str.length(); + final int searchStrLength = searchStr.length(); + if (strLength < searchStrLength) { + // issue#I4M16G@Gitee + return str(str); + } + + if (fromIndex > strLength) { + return str(str); + } else if (fromIndex < 0) { + fromIndex = 0; + } + + final StringBuilder result = new StringBuilder(strLength - searchStrLength + replacement.length()); + if (0 != fromIndex) { + result.append(str.subSequence(0, fromIndex)); + } + + int preIndex = fromIndex; + int index; + while ((index = indexOf(str, searchStr, preIndex, ignoreCase)) > -1) { + result.append(str.subSequence(preIndex, index)); + result.append(replacement); + preIndex = index + searchStrLength; + } + + if (preIndex < strLength) { + // 结尾部分 + result.append(str.subSequence(preIndex, strLength)); + } + return result.toString(); + } + + /** + * 替换指定字符串的指定区间内字符为固定字符
+ * 此方法使用{@link String#codePoints()}完成拆分替换 + * + * @param str 字符串 + * @param startInclude 开始位置(包含) + * @param endExclude 结束位置(不包含) + * @param replacedChar 被替换的字符 + * @return 替换后的字符串 + * @since 3.2.1 + */ + public static String replace(CharSequence str, int startInclude, int endExclude, char replacedChar) { + if (isEmpty(str)) { + return str(str); + } + final String originalStr = str(str); + int[] strCodePoints = originalStr.codePoints().toArray(); + final int strLength = strCodePoints.length; + if (startInclude > strLength) { + return originalStr; + } + if (endExclude > strLength) { + endExclude = strLength; + } + if (startInclude > endExclude) { + // 如果起始位置大于结束位置,不替换 + return originalStr; + } + + final StringBuilder stringBuilder = new StringBuilder(); + for (int i = 0; i < strLength; i++) { + if (i >= startInclude && i < endExclude) { + stringBuilder.append(replacedChar); + } else { + stringBuilder.append(new String(strCodePoints, i, 1)); + } + } + return stringBuilder.toString(); + } + + /** + * 替换指定字符串的指定区间内字符为指定字符串,字符串只重复一次
+ * 此方法使用{@link String#codePoints()}完成拆分替换 + * + * @param str 字符串 + * @param startInclude 开始位置(包含) + * @param endExclude 结束位置(不包含) + * @param replacedStr 被替换的字符串 + * @return 替换后的字符串 + * @since 3.2.1 + */ + public static String replace(CharSequence str, int startInclude, int endExclude, CharSequence replacedStr) { + if (isEmpty(str)) { + return str(str); + } + final String originalStr = str(str); + int[] strCodePoints = originalStr.codePoints().toArray(); + final int strLength = strCodePoints.length; + if (startInclude > strLength) { + return originalStr; + } + if (endExclude > strLength) { + endExclude = strLength; + } + if (startInclude > endExclude) { + // 如果起始位置大于结束位置,不替换 + return originalStr; + } + + final StringBuilder stringBuilder = new StringBuilder(); + for (int i = 0; i < startInclude; i++) { + stringBuilder.append(new String(strCodePoints, i, 1)); + } + stringBuilder.append(replacedStr); + for (int i = endExclude; i < strLength; i++) { + stringBuilder.append(new String(strCodePoints, i, 1)); + } + return stringBuilder.toString(); + } + + /** + * 替换所有正则匹配的文本,并使用自定义函数决定如何替换
+ * replaceFun可以通过{@link Matcher}提取出匹配到的内容的不同部分,然后经过重新处理、组装变成新的内容放回原位。 + *
+	 *     replace(this.content, "(\\d+)", parameters -> "-" + parameters.group(1) + "-")
+	 *     // 结果为:"ZZZaaabbbccc中文-1234-"
+	 * 
+ * + * @param str 要替换的字符串 + * @param pattern 用于匹配的正则式 + * @param replaceFun 决定如何替换的函数 + * @return 替换后的字符串 + * @see ReUtil#replaceAll(CharSequence, java.util.regex.Pattern, Func1) + * @since 4.2.2 + */ + public static String replace(CharSequence str, java.util.regex.Pattern pattern, Func1 replaceFun) { + return ReUtil.replaceAll(str, pattern, replaceFun); + } + + /** + * 替换所有正则匹配的文本,并使用自定义函数决定如何替换 + * + * @param str 要替换的字符串 + * @param regex 用于匹配的正则式 + * @param replaceFun 决定如何替换的函数 + * @return 替换后的字符串 + * @see ReUtil#replaceAll(CharSequence, String, Func1) + * @since 4.2.2 + */ + public static String replace(CharSequence str, String regex, Func1 replaceFun) { + return ReUtil.replaceAll(str, regex, replaceFun); + } + + /** + * 替换字符串中最后一个指定字符串 + * + * @param str 字符串 + * @param searchStr 被查找的字符串 + * @param replacedStr 被替换的字符串 + * @return 替换后的字符串 + */ + public static String replaceLast(CharSequence str, CharSequence searchStr, CharSequence replacedStr) { + return replaceLast(str, searchStr, replacedStr, false); + } + + /** + * 替换字符串中最后一个指定字符串 + * + * @param str 字符串 + * @param searchStr 被查找的字符串 + * @param replacedStr 被替换的字符串 + * @param ignoreCase 是否忽略大小写 + * @return 替换后的字符串 + */ + public static String replaceLast(CharSequence str, CharSequence searchStr, CharSequence replacedStr, boolean ignoreCase) { + if (isEmpty(str)) { + return str(str); + } + int lastIndex = lastIndexOf(str, searchStr, str.length(), ignoreCase); + if (INDEX_NOT_FOUND == lastIndex) { + return str(str); + } + return replace(str, lastIndex, searchStr, replacedStr, ignoreCase); + } + + /** + * 替换字符串中第一个指定字符串 + * + * @param str 字符串 + * @param searchStr 被查找的字符串 + * @param replacedStr 被替换的字符串 + * @return 替换后的字符串 + */ + public static String replaceFirst(CharSequence str, CharSequence searchStr, CharSequence replacedStr) { + return replaceFirst(str, searchStr, replacedStr, false); + } + + /** + * 替换字符串中第一个指定字符串 + * + * @param str 字符串 + * @param searchStr 被查找的字符串 + * @param replacedStr 被替换的字符串 + * @param ignoreCase 是否忽略大小写 + * @return 替换后的字符串 + */ + public static String replaceFirst(CharSequence str, CharSequence searchStr, CharSequence replacedStr, boolean ignoreCase) { + if (isEmpty(str)) { + return str(str); + } + int startInclude = indexOf(str, searchStr, 0, ignoreCase); + if (INDEX_NOT_FOUND == startInclude) { + return str(str); + } + return replace(str, startInclude, startInclude + searchStr.length(), replacedStr); + } + + /** + * 替换指定字符串的指定区间内字符为"*" + * 俗称:脱敏功能,后面其他功能,可以见:DesensitizedUtil(脱敏工具类) + * + *
+	 * StrUtil.hide(null,*,*)=null
+	 * StrUtil.hide("",0,*)=""
+	 * StrUtil.hide("jackduan@163.com",-1,4)   ****duan@163.com
+	 * StrUtil.hide("jackduan@163.com",2,3)    ja*kduan@163.com
+	 * StrUtil.hide("jackduan@163.com",3,2)    jackduan@163.com
+	 * StrUtil.hide("jackduan@163.com",16,16)  jackduan@163.com
+	 * StrUtil.hide("jackduan@163.com",16,17)  jackduan@163.com
+	 * 
+ * + * @param str 字符串 + * @param startInclude 开始位置(包含) + * @param endExclude 结束位置(不包含) + * @return 替换后的字符串 + * @since 4.1.14 + */ + public static String hide(CharSequence str, int startInclude, int endExclude) { + return replace(str, startInclude, endExclude, '*'); + } + + /** + * 脱敏,使用默认的脱敏策略 + * + *
+	 * StrUtil.desensitized("100", DesensitizedUtil.DesensitizedType.USER_ID)) =  "0"
+	 * StrUtil.desensitized("段正淳", DesensitizedUtil.DesensitizedType.CHINESE_NAME)) = "段**"
+	 * StrUtil.desensitized("51343620000320711X", DesensitizedUtil.DesensitizedType.ID_CARD)) = "5***************1X"
+	 * StrUtil.desensitized("09157518479", DesensitizedUtil.DesensitizedType.FIXED_PHONE)) = "0915*****79"
+	 * StrUtil.desensitized("18049531999", DesensitizedUtil.DesensitizedType.MOBILE_PHONE)) = "180****1999"
+	 * StrUtil.desensitized("北京市海淀区马连洼街道289号", DesensitizedUtil.DesensitizedType.ADDRESS)) = "北京市海淀区马********"
+	 * StrUtil.desensitized("duandazhi-jack@gmail.com.cn", DesensitizedUtil.DesensitizedType.EMAIL)) = "d*************@gmail.com.cn"
+	 * StrUtil.desensitized("1234567890", DesensitizedUtil.DesensitizedType.PASSWORD)) = "**********"
+	 * StrUtil.desensitized("苏D40000", DesensitizedUtil.DesensitizedType.CAR_LICENSE)) = "苏D4***0"
+	 * StrUtil.desensitized("11011111222233333256", DesensitizedType.BANK_CARD)) = "1101 **** **** **** 3256"
+	 * 
+ * + * @param str 字符串 + * @param desensitizedType 脱敏类型;可以脱敏:用户id、中文名、身份证号、座机号、手机号、地址、电子邮件、密码 + * @return 脱敏之后的字符串 + * @author dazer and neusoft and qiaomu + * @see DesensitizedUtil 如果需要自定义,脱敏规则,请使用该工具类; + * @since 5.6.2 + */ + public static String desensitized(CharSequence str, DesensitizedUtil.DesensitizedType desensitizedType) { + return DesensitizedUtil.desensitized(str, desensitizedType); + } + + /** + * 替换字符字符数组中所有的字符为replacedStr
+ * 提供的chars为所有需要被替换的字符,例如:"\r\n",则"\r"和"\n"都会被替换,哪怕他们单独存在 + * + * @param str 被检查的字符串 + * @param chars 需要替换的字符列表,用一个字符串表示这个字符列表 + * @param replacedStr 替换成的字符串 + * @return 新字符串 + * @since 3.2.2 + */ + public static String replaceChars(CharSequence str, String chars, CharSequence replacedStr) { + if (isEmpty(str) || isEmpty(chars)) { + return str(str); + } + return replaceChars(str, chars.toCharArray(), replacedStr); + } + + /** + * 替换字符字符数组中所有的字符为replacedStr + * + * @param str 被检查的字符串 + * @param chars 需要替换的字符列表 + * @param replacedStr 替换成的字符串 + * @return 新字符串 + * @since 3.2.2 + */ + public static String replaceChars(CharSequence str, char[] chars, CharSequence replacedStr) { + if (isEmpty(str) || ArrayUtil.isEmpty(chars)) { + return str(str); + } + + final Set set = new HashSet<>(chars.length); + for (char c : chars) { + set.add(c); + } + int strLen = str.length(); + final StringBuilder builder = new StringBuilder(); + char c; + for (int i = 0; i < strLen; i++) { + c = str.charAt(i); + builder.append(set.contains(c) ? replacedStr : c); + } + return builder.toString(); + } + + // ------------------------------------------------------------------------ length + + /** + * 获取字符串的长度,如果为null返回0 + * + * @param cs a 字符串 + * @return 字符串的长度,如果为null返回0 + * @since 4.3.2 + */ + public static int length(CharSequence cs) { + return cs == null ? 0 : cs.length(); + } + + /** + * 给定字符串转为bytes后的byte数(byte长度) + * + * @param cs 字符串 + * @param charset 编码 + * @return byte长度 + * @since 4.5.2 + */ + public static int byteLength(CharSequence cs, Charset charset) { + return cs == null ? 0 : cs.toString().getBytes(charset).length; + } + + /** + * 给定字符串数组的总长度
+ * null字符长度定义为0 + * + * @param strs 字符串数组 + * @return 总长度 + * @since 4.0.1 + */ + public static int totalLength(CharSequence... strs) { + int totalLength = 0; + for (CharSequence str : strs) { + totalLength += (null == str ? 0 : str.length()); + } + return totalLength; + } + + /** + * 限制字符串长度,如果超过指定长度,截取指定长度并在末尾加"..." + * + * @param string 字符串 + * @param length 最大长度 + * @return 切割后的剩余的前半部分字符串+"..." + * @since 4.0.10 + */ + public static String maxLength(CharSequence string, int length) { + Assert.isTrue(length > 0); + if (null == string) { + return null; + } + if (string.length() <= length) { + return string.toString(); + } + return sub(string, 0, length) + "..."; + } + + // ------------------------------------------------------------------------ firstXXX + + /** + * 返回第一个非{@code null} 元素 + * + * @param strs 多个元素 + * @param 元素类型 + * @return 第一个非空元素,如果给定的数组为空或者都为空,返回{@code null} + * @since 5.4.1 + */ + @SuppressWarnings("unchecked") + public static T firstNonNull(T... strs) { + return ArrayUtil.firstNonNull(strs); + } + + /** + * 返回第一个非empty 元素 + * + * @param strs 多个元素 + * @param 元素类型 + * @return 第一个非空元素,如果给定的数组为空或者都为空,返回{@code null} + * @see #isNotEmpty(CharSequence) + * @since 5.4.1 + */ + @SuppressWarnings("unchecked") + public static T firstNonEmpty(T... strs) { + return ArrayUtil.firstMatch(StrUtil::isNotEmpty, strs); + } + + /** + * 返回第一个非blank 元素 + * + * @param strs 多个元素 + * @param 元素类型 + * @return 第一个非空元素,如果给定的数组为空或者都为空,返回{@code null} + * @see #isNotBlank(CharSequence) + * @since 5.4.1 + */ + @SuppressWarnings("unchecked") + public static T firstNonBlank(T... strs) { + return ArrayUtil.firstMatch(StrUtil::isNotBlank, strs); + } + + // ------------------------------------------------------------------------ lower and upper + + /** + * 原字符串首字母大写并在其首部添加指定字符串 例如:str=name, preString=get =》 return getName + * + * @param str 被处理的字符串 + * @param preString 添加的首部 + * @return 处理后的字符串 + */ + public static String upperFirstAndAddPre(CharSequence str, String preString) { + if (str == null || preString == null) { + return null; + } + return preString + upperFirst(str); + } + + /** + * 大写首字母
+ * 例如:str = name, return Name + * + * @param str 字符串 + * @return 字符串 + */ + public static String upperFirst(CharSequence str) { + if (null == str) { + return null; + } + if (str.length() > 0) { + char firstChar = str.charAt(0); + if (Character.isLowerCase(firstChar)) { + return Character.toUpperCase(firstChar) + subSuf(str, 1); + } + } + return str.toString(); + } + + /** + * 小写首字母
+ * 例如:str = Name, return name + * + * @param str 字符串 + * @return 字符串 + */ + public static String lowerFirst(CharSequence str) { + if (null == str) { + return null; + } + if (str.length() > 0) { + char firstChar = str.charAt(0); + if (Character.isUpperCase(firstChar)) { + return Character.toLowerCase(firstChar) + subSuf(str, 1); + } + } + return str.toString(); + } + + // ------------------------------------------------------------------------ filter + + /** + * 过滤字符串 + * + * @param str 字符串 + * @param filter 过滤器,{@link Filter#accept(Object)}返回为{@code true}的保留字符 + * @return 过滤后的字符串 + * @since 5.4.0 + */ + public static String filter(CharSequence str, final Filter filter) { + if (str == null || filter == null) { + return str(str); + } + + int len = str.length(); + final StringBuilder sb = new StringBuilder(len); + char c; + for (int i = 0; i < len; i++) { + c = str.charAt(i); + if (filter.accept(c)) { + sb.append(c); + } + } + return sb.toString(); + } + + // ------------------------------------------------------------------------ case + + /** + * 给定字符串中的字母是否全部为大写,判断依据如下: + * + *
+	 * 1. 大写字母包括A-Z
+	 * 2. 其它非字母的Unicode符都算作大写
+	 * 
+ * + * @param str 被检查的字符串 + * @return 是否全部为大写 + * @since 4.2.2 + */ + public static boolean isUpperCase(CharSequence str) { + if (null == str) { + return false; + } + final int len = str.length(); + for (int i = 0; i < len; i++) { + if (Character.isLowerCase(str.charAt(i))) { + return false; + } + } + return true; + } + + /** + * 给定字符串中的字母是否全部为小写,判断依据如下: + * + *
+	 * 1. 小写字母包括a-z
+	 * 2. 其它非字母的Unicode符都算作小写
+	 * 
+ * + * @param str 被检查的字符串 + * @return 是否全部为小写 + * @since 4.2.2 + */ + public static boolean isLowerCase(CharSequence str) { + if (null == str) { + return false; + } + final int len = str.length(); + for (int i = 0; i < len; i++) { + if (Character.isUpperCase(str.charAt(i))) { + return false; + } + } + return true; + } + + /** + * 切换给定字符串中的大小写。大写转小写,小写转大写。 + * + *
+	 * StrUtil.swapCase(null)                 = null
+	 * StrUtil.swapCase("")                   = ""
+	 * StrUtil.swapCase("The dog has a BONE") = "tHE DOG HAS A bone"
+	 * 
+ * + * @param str 字符串 + * @return 交换后的字符串 + * @since 4.3.2 + */ + public static String swapCase(final String str) { + if (isEmpty(str)) { + return str; + } + + final char[] buffer = str.toCharArray(); + + for (int i = 0; i < buffer.length; i++) { + final char ch = buffer[i]; + if (Character.isUpperCase(ch)) { + buffer[i] = Character.toLowerCase(ch); + } else if (Character.isTitleCase(ch)) { + buffer[i] = Character.toLowerCase(ch); + } else if (Character.isLowerCase(ch)) { + buffer[i] = Character.toUpperCase(ch); + } + } + return new String(buffer); + } + + /** + * 将驼峰式命名的字符串转换为下划线方式。如果转换前的驼峰式命名的字符串为空,则返回空字符串。
+ * 例如: + * + *
+	 * HelloWorld=》hello_world
+	 * Hello_World=》hello_world
+	 * HelloWorld_test=》hello_world_test
+	 * 
+ * + * @param str 转换前的驼峰式命名的字符串,也可以为下划线形式 + * @return 转换后下划线方式命名的字符串 + * @see NamingCase#toUnderlineCase(CharSequence) + */ + public static String toUnderlineCase(CharSequence str) { + return NamingCase.toUnderlineCase(str); + } + + /** + * 将驼峰式命名的字符串转换为使用符号连接方式。如果转换前的驼峰式命名的字符串为空,则返回空字符串。
+ * + * @param str 转换前的驼峰式命名的字符串,也可以为符号连接形式 + * @param symbol 连接符 + * @return 转换后符号连接方式命名的字符串 + * @see NamingCase#toSymbolCase(CharSequence, char) + * @since 4.0.10 + */ + public static String toSymbolCase(CharSequence str, char symbol) { + return NamingCase.toSymbolCase(str, symbol); + } + + /** + * 将下划线方式命名的字符串转换为驼峰式。如果转换前的下划线大写方式命名的字符串为空,则返回空字符串。
+ * 例如:hello_world=》helloWorld + * + * @param name 转换前的下划线大写方式命名的字符串 + * @return 转换后的驼峰式命名的字符串 + * @see NamingCase#toCamelCase(CharSequence) + */ + public static String toCamelCase(CharSequence name) { + return NamingCase.toCamelCase(name); + } + + /** + * 将连接符方式命名的字符串转换为驼峰式。如果转换前的下划线大写方式命名的字符串为空,则返回空字符串。
+ * 例如:hello_world=》helloWorld; hello-world=》helloWorld + * + * @param name 转换前的下划线大写方式命名的字符串 + * @param symbol 连接符 + * @return 转换后的驼峰式命名的字符串 + * @see NamingCase#toCamelCase(CharSequence, char) + */ + public static String toCamelCase(CharSequence name, char symbol) { + return NamingCase.toCamelCase(name, symbol); + } + + // ------------------------------------------------------------------------ isSurround + + /** + * 给定字符串是否被字符包围 + * + * @param str 字符串 + * @param prefix 前缀 + * @param suffix 后缀 + * @return 是否包围,空串不包围 + */ + public static boolean isSurround(CharSequence str, CharSequence prefix, CharSequence suffix) { + if (StrUtil.isBlank(str)) { + return false; + } + if (str.length() < (prefix.length() + suffix.length())) { + return false; + } + + final String str2 = str.toString(); + return str2.startsWith(prefix.toString()) && str2.endsWith(suffix.toString()); + } + + /** + * 给定字符串是否被字符包围 + * + * @param str 字符串 + * @param prefix 前缀 + * @param suffix 后缀 + * @return 是否包围,空串不包围 + */ + public static boolean isSurround(CharSequence str, char prefix, char suffix) { + if (StrUtil.isBlank(str)) { + return false; + } + if (str.length() < 2) { + return false; + } + + return str.charAt(0) == prefix && str.charAt(str.length() - 1) == suffix; + } + + // ------------------------------------------------------------------------ builder + + /** + * 创建StringBuilder对象 + * + * @param strs 初始字符串列表 + * @return StringBuilder对象 + */ + public static StringBuilder builder(CharSequence... strs) { + final StringBuilder sb = new StringBuilder(); + for (CharSequence str : strs) { + sb.append(str); + } + return sb; + } + + /** + * 创建StrBuilder对象 + * + * @param strs 初始字符串列表 + * @return StrBuilder对象 + */ + public static StrBuilder strBuilder(CharSequence... strs) { + return StrBuilder.create(strs); + } + + // ------------------------------------------------------------------------ getter and setter + + /** + * 获得set或get或is方法对应的标准属性名
+ * 例如:setName 返回 name + * + *
+	 * getName =》name
+	 * setName =》name
+	 * isName  =》name
+	 * 
+ * + * @param getOrSetMethodName Get或Set方法名 + * @return 如果是set或get方法名,返回field, 否则null + */ + public static String getGeneralField(CharSequence getOrSetMethodName) { + final String getOrSetMethodNameStr = getOrSetMethodName.toString(); + if (getOrSetMethodNameStr.startsWith("get") || getOrSetMethodNameStr.startsWith("set")) { + return removePreAndLowerFirst(getOrSetMethodName, 3); + } else if (getOrSetMethodNameStr.startsWith("is")) { + return removePreAndLowerFirst(getOrSetMethodName, 2); + } + return null; + } + + /** + * 生成set方法名
+ * 例如:name 返回 setName + * + * @param fieldName 属性名 + * @return setXxx + */ + public static String genSetter(CharSequence fieldName) { + return upperFirstAndAddPre(fieldName, "set"); + } + + /** + * 生成get方法名 + * + * @param fieldName 属性名 + * @return getXxx + */ + public static String genGetter(CharSequence fieldName) { + return upperFirstAndAddPre(fieldName, "get"); + } + + // ------------------------------------------------------------------------ other + + /** + * 连接多个字符串为一个 + * + * @param isNullToEmpty 是否null转为"" + * @param strs 字符串数组 + * @return 连接后的字符串 + * @since 4.1.0 + */ + public static String concat(boolean isNullToEmpty, CharSequence... strs) { + final StrBuilder sb = new StrBuilder(); + for (CharSequence str : strs) { + sb.append(isNullToEmpty ? nullToEmpty(str) : str); + } + return sb.toString(); + } + + /** + * 将给定字符串,变成 "xxx...xxx" 形式的字符串 + * + *
    + *
  • abcdefgh 9 -》 abcdefgh
  • + *
  • abcdefgh 8 -》 abcdefgh
  • + *
  • abcdefgh 7 -》 ab...gh
  • + *
  • abcdefgh 6 -》 ab...h
  • + *
  • abcdefgh 5 -》 a...h
  • + *
  • abcdefgh 4 -》 a..h
  • + *
  • abcdefgh 3 -》 a.h
  • + *
  • abcdefgh 2 -》 a.
  • + *
  • abcdefgh 1 -》 a
  • + *
  • abcdefgh 0 -》 abcdefgh
  • + *
  • abcdefgh -1 -》 abcdefgh
  • + *
+ * + * @param str 字符串 + * @param maxLength 结果的最大长度 + * @return 截取后的字符串 + */ + public static String brief(CharSequence str, int maxLength) { + if (null == str) { + return null; + } + final int strLength = str.length(); + if (maxLength <= 0 || strLength <= maxLength) { + return str.toString(); + } + + // since 5.7.5,特殊长度 + switch (maxLength) { + case 1: + return String.valueOf(str.charAt(0)); + case 2: + return str.charAt(0) + "."; + case 3: + return str.charAt(0) + "." + str.charAt(strLength - 1); + case 4: + return str.charAt(0) + ".." + str.charAt(strLength - 1); + } + + final int suffixLength = (maxLength - 3) / 2; + final int preLength = suffixLength + (maxLength - 3) % 2; // suffixLength 或 suffixLength + 1 + final String str2 = str.toString(); + return format("{}...{}", + str2.substring(0, preLength), + str2.substring(strLength - suffixLength)); + } + + /** + * 以 conjunction 为分隔符将多个对象转换为字符串 + * + * @param conjunction 分隔符 {@link StrPool#COMMA} + * @param objs 数组 + * @return 连接后的字符串 + * @see ArrayUtil#join(Object, CharSequence) + */ + public static String join(CharSequence conjunction, Object... objs) { + return ArrayUtil.join(objs, conjunction); + } + + /** + * 以 conjunction 为分隔符将多个对象转换为字符串 + * + * @param 元素类型 + * @param conjunction 分隔符 {@link StrPool#COMMA} + * @param iterable 集合 + * @return 连接后的字符串 + * @see CollUtil#join(Iterable, CharSequence) + * @since 5.6.6 + */ + public static String join(CharSequence conjunction, Iterable iterable) { + return CollUtil.join(iterable, conjunction); + } + + /** + * 字符串的每一个字符是否都与定义的匹配器匹配 + * + * @param value 字符串 + * @param matcher 匹配器 + * @return 是否全部匹配 + * @since 3.2.3 + */ + public static boolean isAllCharMatch(CharSequence value, Matcher matcher) { + if (StrUtil.isBlank(value)) { + return false; + } + for (int i = value.length(); --i >= 0; ) { + if (!matcher.match(value.charAt(i))) { + return false; + } + } + return true; + } + + /** + * 检查字符串是否都为数字组成 + * + * @param str 字符串 + * @return 是否都为数字组成 + * @since 5.7.3 + */ + public static boolean isNumeric(CharSequence str) { + return isAllCharMatch(str, Character::isDigit); + } + + /** + * 循环位移指定位置的字符串为指定距离
+ * 当moveLength大于0向右位移,小于0向左位移,0不位移
+ * 当moveLength大于字符串长度时采取循环位移策略,即位移到头后从头(尾)位移,例如长度为10,位移13则表示位移3 + * + * @param str 字符串 + * @param startInclude 起始位置(包括) + * @param endExclude 结束位置(不包括) + * @param moveLength 移动距离,负数表示左移,正数为右移 + * @return 位移后的字符串 + * @since 4.0.7 + */ + public static String move(CharSequence str, int startInclude, int endExclude, int moveLength) { + if (isEmpty(str)) { + return str(str); + } + int len = str.length(); + if (Math.abs(moveLength) > len) { + // 循环位移,当越界时循环 + moveLength = moveLength % len; + } + final StringBuilder strBuilder = new StringBuilder(len); + if (moveLength > 0) { + int endAfterMove = Math.min(endExclude + moveLength, str.length()); + strBuilder.append(str.subSequence(0, startInclude))// + .append(str.subSequence(endExclude, endAfterMove))// + .append(str.subSequence(startInclude, endExclude))// + .append(str.subSequence(endAfterMove, str.length())); + } else if (moveLength < 0) { + int startAfterMove = Math.max(startInclude + moveLength, 0); + strBuilder.append(str.subSequence(0, startAfterMove))// + .append(str.subSequence(startInclude, endExclude))// + .append(str.subSequence(startAfterMove, startInclude))// + .append(str.subSequence(endExclude, str.length())); + } else { + return str(str); + } + return strBuilder.toString(); + } + + /** + * 检查给定字符串的所有字符是否都一样 + * + * @param str 字符出啊 + * @return 给定字符串的所有字符是否都一样 + * @since 5.7.3 + */ + public static boolean isCharEquals(CharSequence str) { + Assert.notEmpty(str, "Str to check must be not empty!"); + return count(str, str.charAt(0)) == str.length(); + } + + /** + * 对字符串归一化处理,如 "Á" 可以使用 "u00C1"或 "u0041u0301"表示,实际测试中两个字符串并不equals
+ * 因此使用此方法归一为一种表示形式,默认按照W3C通常建议的,在NFC中交换文本。 + * + * @param str 归一化的字符串 + * @return 归一化后的字符串 + * @see Normalizer#normalize(CharSequence, Normalizer.Form) + * @since 5.7.16 + */ + public static String normalize(CharSequence str) { + return Normalizer.normalize(str, Normalizer.Form.NFC); + } + + /** + * 在给定字符串末尾填充指定字符,以达到给定长度
+ * 如果字符串本身的长度大于等于length,返回原字符串 + * + * @param str 字符串 + * @param fixedChar 补充的字符 + * @param length 补充到的长度 + * @return 补充后的字符串 + * @since 5.8.0 + */ + public static String fixLength(CharSequence str, char fixedChar, int length) { + final int fixedLength = length - str.length(); + if (fixedLength <= 0) { + return str.toString(); + } + return str + repeat(fixedChar, fixedLength); + } + + /** + *

指定字符串数组中,是否包含空字符串。

+ *

如果传入参数对象不是为空,则返回false。如果字符串包含字母,不区分大小写,则返回true

+ * + * @param str 对象 + * @return 如果为字符串, 是否有字母 + */ + public static boolean hasLetter(CharSequence str) { + if (null == str) { + return false; + } + for (int i = 0; i < str.length(); i++) { + if (CharUtil.isLetter(str.charAt(i))) { + return true; + } + } + return false; + } + +} diff --git a/src/main/java/cn/hutool/core/text/NamingCase.java b/src/main/java/cn/hutool/core/text/NamingCase.java new file mode 100644 index 0000000..56eb04c --- /dev/null +++ b/src/main/java/cn/hutool/core/text/NamingCase.java @@ -0,0 +1,192 @@ +package cn.hutool.core.text; + +import cn.hutool.core.util.CharUtil; +import cn.hutool.core.util.StrUtil; + +/** + * 命名规则封装,主要是针对驼峰风格命名、连接符命名等的封装 + * + * @author looly + * @since 5.7.10 + */ +public class NamingCase { + + /** + * 将驼峰式命名的字符串转换为下划线方式,又称SnakeCase、underScoreCase。
+ * 如果转换前的驼峰式命名的字符串为空,则返回空字符串。
+ * 规则为: + *
    + *
  • 单字之间以下划线隔开
  • + *
  • 每个单字的首字母亦用小写字母
  • + *
+ * 例如: + * + *
+	 * HelloWorld=》hello_world
+	 * Hello_World=》hello_world
+	 * HelloWorld_test=》hello_world_test
+	 * 
+ * + * @param str 转换前的驼峰式命名的字符串,也可以为下划线形式 + * @return 转换后下划线方式命名的字符串 + */ + public static String toUnderlineCase(CharSequence str) { + return toSymbolCase(str, CharUtil.UNDERLINE); + } + + /** + * 将驼峰式命名的字符串转换为短横连接方式。
+ * 如果转换前的驼峰式命名的字符串为空,则返回空字符串。
+ * 规则为: + *
    + *
  • 单字之间横线线隔开
  • + *
  • 每个单字的首字母亦用小写字母
  • + *
+ * 例如: + * + *
+	 * HelloWorld=》hello-world
+	 * Hello_World=》hello-world
+	 * HelloWorld_test=》hello-world-test
+	 * 
+ * + * @param str 转换前的驼峰式命名的字符串,也可以为下划线形式 + * @return 转换后下划线方式命名的字符串 + */ + public static String toKebabCase(CharSequence str) { + return toSymbolCase(str, CharUtil.DASHED); + } + + /** + * 将驼峰式命名的字符串转换为使用符号连接方式。如果转换前的驼峰式命名的字符串为空,则返回空字符串。 + * + * @param str 转换前的驼峰式命名的字符串,也可以为符号连接形式 + * @param symbol 连接符 + * @return 转换后符号连接方式命名的字符串 + * @since 4.0.10 + */ + public static String toSymbolCase(CharSequence str, char symbol) { + if (str == null) { + return null; + } + + final int length = str.length(); + final StrBuilder sb = new StrBuilder(); + char c; + for (int i = 0; i < length; i++) { + c = str.charAt(i); + if (Character.isUpperCase(c)) { + final Character preChar = (i > 0) ? str.charAt(i - 1) : null; + final Character nextChar = (i < str.length() - 1) ? str.charAt(i + 1) : null; + + if (null != preChar) { + if (symbol == preChar) { + // 前一个为分隔符 + if (null == nextChar || Character.isLowerCase(nextChar)) { + //普通首字母大写,如_Abb -> _abb + c = Character.toLowerCase(c); + } + //后一个为大写,按照专有名词对待,如_AB -> _AB + } else if (Character.isLowerCase(preChar)) { + // 前一个为小写 + sb.append(symbol); + if (null == nextChar || Character.isLowerCase(nextChar) || CharUtil.isNumber(nextChar)) { + //普通首字母大写,如aBcc -> a_bcc + c = Character.toLowerCase(c); + } + // 后一个为大写,按照专有名词对待,如aBC -> a_BC + } else { + //前一个为大写 + if (null != nextChar && Character.isLowerCase(nextChar)) { + // 普通首字母大写,如ABcc -> A_bcc + sb.append(symbol); + c = Character.toLowerCase(c); + } + // 后一个为大写,按照专有名词对待,如ABC -> ABC + } + } else { + // 首字母,需要根据后一个判断是否转为小写 + if (null == nextChar || Character.isLowerCase(nextChar)) { + // 普通首字母大写,如Abc -> abc + c = Character.toLowerCase(c); + } + // 后一个为大写,按照专有名词对待,如ABC -> ABC + } + } + sb.append(c); + } + return sb.toString(); + } + + /** + * 将下划线方式命名的字符串转换为帕斯卡式。
+ * 规则为: + *
    + *
  • 单字之间不以空格或任何连接符断开
  • + *
  • 第一个单字首字母采用大写字母
  • + *
  • 后续单字的首字母亦用大写字母
  • + *
+ * 如果转换前的下划线大写方式命名的字符串为空,则返回空字符串。
+ * 例如:hello_world=》HelloWorld + * + * @param name 转换前的下划线大写方式命名的字符串 + * @return 转换后的驼峰式命名的字符串 + */ + public static String toPascalCase(CharSequence name) { + return StrUtil.upperFirst(toCamelCase(name)); + } + + /** + * 将下划线方式命名的字符串转换为驼峰式。如果转换前的下划线大写方式命名的字符串为空,则返回空字符串。
+ * 规则为: + *
    + *
  • 单字之间不以空格或任何连接符断开
  • + *
  • 第一个单字首字母采用小写字母
  • + *
  • 后续单字的首字母亦用大写字母
  • + *
+ * 例如:hello_world=》helloWorld + * + * @param name 转换前的下划线大写方式命名的字符串 + * @return 转换后的驼峰式命名的字符串 + */ + public static String toCamelCase(CharSequence name) { + return toCamelCase(name, CharUtil.UNDERLINE); + } + + /** + * 将连接符方式命名的字符串转换为驼峰式。如果转换前的下划线大写方式命名的字符串为空,则返回空字符串。 + * + * @param name 转换前的自定义方式命名的字符串 + * @param symbol 原字符串中的连接符连接符 + * @return 转换后的驼峰式命名的字符串 + * @since 5.7.17 + */ + public static String toCamelCase(CharSequence name, char symbol) { + if (null == name) { + return null; + } + + final String name2 = name.toString(); + if (StrUtil.contains(name2, symbol)) { + final int length = name2.length(); + final StringBuilder sb = new StringBuilder(length); + boolean upperCase = false; + for (int i = 0; i < length; i++) { + char c = name2.charAt(i); + + if (c == symbol) { + upperCase = true; + } else if (upperCase) { + sb.append(Character.toUpperCase(c)); + upperCase = false; + } else { + sb.append(Character.toLowerCase(c)); + } + } + return sb.toString(); + } else { + return name2; + } + } + +} diff --git a/src/main/java/cn/hutool/core/text/PasswdStrength.java b/src/main/java/cn/hutool/core/text/PasswdStrength.java new file mode 100644 index 0000000..d480573 --- /dev/null +++ b/src/main/java/cn/hutool/core/text/PasswdStrength.java @@ -0,0 +1,288 @@ +package cn.hutool.core.text; + +import cn.hutool.core.util.StrUtil; + +/** + * 检测密码强度
+ * 来自:https://github.com/venshine/CheckPasswordStrength + * + * @author venshine + * @since 5.7.3 + */ +public class PasswdStrength { + + /** + * 密码等级枚举 + */ + public enum PASSWD_LEVEL { + EASY, MIDIUM, STRONG, VERY_STRONG, EXTREMELY_STRONG + } + + /** + * 字符类型枚举 + */ + public enum CHAR_TYPE { + NUM, SMALL_LETTER, CAPITAL_LETTER, OTHER_CHAR + } + + /** + * 简单密码字典 + */ + private final static String[] DICTIONARY = {"password", "abc123", "iloveyou", "adobe123", "123123", "sunshine", + "1314520", "a1b2c3", "123qwe", "aaa111", "qweasd", "admin", "passwd"}; + + /** + * 数字长度 + */ + private final static int[] SIZE_TABLE = {9, 99, 999, 9999, 99999, 999999, 9999999, 99999999, 999999999, + Integer.MAX_VALUE}; + + /** + * 检查密码的健壮性 + * + * @param passwd 密码 + * @return strength level + */ + public static int check(String passwd) { + if (null == passwd) { + throw new IllegalArgumentException("password is empty"); + } + int len = passwd.length(); + int level = 0; + + // increase points + if (countLetter(passwd, CHAR_TYPE.NUM) > 0) { + level++; + } + if (countLetter(passwd, CHAR_TYPE.SMALL_LETTER) > 0) { + level++; + } + if (len > 4 && countLetter(passwd, CHAR_TYPE.CAPITAL_LETTER) > 0) { + level++; + } + if (len > 6 && countLetter(passwd, CHAR_TYPE.OTHER_CHAR) > 0) { + level++; + } + + if (len > 4 && countLetter(passwd, CHAR_TYPE.NUM) > 0 && countLetter(passwd, CHAR_TYPE.SMALL_LETTER) > 0 + || countLetter(passwd, CHAR_TYPE.NUM) > 0 && countLetter(passwd, CHAR_TYPE.CAPITAL_LETTER) > 0 + || countLetter(passwd, CHAR_TYPE.NUM) > 0 && countLetter(passwd, CHAR_TYPE.OTHER_CHAR) > 0 + || countLetter(passwd, CHAR_TYPE.SMALL_LETTER) > 0 && countLetter(passwd, CHAR_TYPE.CAPITAL_LETTER) > 0 + || countLetter(passwd, CHAR_TYPE.SMALL_LETTER) > 0 && countLetter(passwd, CHAR_TYPE.OTHER_CHAR) > 0 + || countLetter(passwd, CHAR_TYPE.CAPITAL_LETTER) > 0 && countLetter(passwd, CHAR_TYPE.OTHER_CHAR) > 0) { + level++; + } + + if (len > 6 && countLetter(passwd, CHAR_TYPE.NUM) > 0 && countLetter(passwd, CHAR_TYPE.SMALL_LETTER) > 0 + && countLetter(passwd, CHAR_TYPE.CAPITAL_LETTER) > 0 || countLetter(passwd, CHAR_TYPE.NUM) > 0 + && countLetter(passwd, CHAR_TYPE.SMALL_LETTER) > 0 && countLetter(passwd, CHAR_TYPE.OTHER_CHAR) > 0 + || countLetter(passwd, CHAR_TYPE.NUM) > 0 && countLetter(passwd, CHAR_TYPE.CAPITAL_LETTER) > 0 + && countLetter(passwd, CHAR_TYPE.OTHER_CHAR) > 0 || countLetter(passwd, CHAR_TYPE.SMALL_LETTER) > 0 + && countLetter(passwd, CHAR_TYPE.CAPITAL_LETTER) > 0 && countLetter(passwd, CHAR_TYPE.OTHER_CHAR) > 0) { + level++; + } + + if (len > 8 && countLetter(passwd, CHAR_TYPE.NUM) > 0 && countLetter(passwd, CHAR_TYPE.SMALL_LETTER) > 0 + && countLetter(passwd, CHAR_TYPE.CAPITAL_LETTER) > 0 && countLetter(passwd, CHAR_TYPE.OTHER_CHAR) > 0) { + level++; + } + + if (len > 6 && countLetter(passwd, CHAR_TYPE.NUM) >= 3 && countLetter(passwd, CHAR_TYPE.SMALL_LETTER) >= 3 + || countLetter(passwd, CHAR_TYPE.NUM) >= 3 && countLetter(passwd, CHAR_TYPE.CAPITAL_LETTER) >= 3 + || countLetter(passwd, CHAR_TYPE.NUM) >= 3 && countLetter(passwd, CHAR_TYPE.OTHER_CHAR) >= 2 + || countLetter(passwd, CHAR_TYPE.SMALL_LETTER) >= 3 && countLetter(passwd, CHAR_TYPE.CAPITAL_LETTER) >= 3 + || countLetter(passwd, CHAR_TYPE.SMALL_LETTER) >= 3 && countLetter(passwd, CHAR_TYPE.OTHER_CHAR) >= 2 + || countLetter(passwd, CHAR_TYPE.CAPITAL_LETTER) >= 3 && countLetter(passwd, CHAR_TYPE.OTHER_CHAR) >= 2) { + level++; + } + + if (len > 8 && countLetter(passwd, CHAR_TYPE.NUM) >= 2 && countLetter(passwd, CHAR_TYPE.SMALL_LETTER) >= 2 + && countLetter(passwd, CHAR_TYPE.CAPITAL_LETTER) >= 2 || countLetter(passwd, CHAR_TYPE.NUM) >= 2 + && countLetter(passwd, CHAR_TYPE.SMALL_LETTER) >= 2 && countLetter(passwd, CHAR_TYPE.OTHER_CHAR) >= 2 + || countLetter(passwd, CHAR_TYPE.NUM) >= 2 && countLetter(passwd, CHAR_TYPE.CAPITAL_LETTER) >= 2 + && countLetter(passwd, CHAR_TYPE.OTHER_CHAR) >= 2 || countLetter(passwd, CHAR_TYPE.SMALL_LETTER) >= 2 + && countLetter(passwd, CHAR_TYPE.CAPITAL_LETTER) >= 2 && countLetter(passwd, CHAR_TYPE.OTHER_CHAR) >= 2) { + level++; + } + + if (len > 10 && countLetter(passwd, CHAR_TYPE.NUM) >= 2 && countLetter(passwd, CHAR_TYPE.SMALL_LETTER) >= 2 + && countLetter(passwd, CHAR_TYPE.CAPITAL_LETTER) >= 2 && countLetter(passwd, CHAR_TYPE.OTHER_CHAR) >= 2) { + level++; + } + + if (countLetter(passwd, CHAR_TYPE.OTHER_CHAR) >= 3) { + level++; + } + if (countLetter(passwd, CHAR_TYPE.OTHER_CHAR) >= 6) { + level++; + } + + if (len > 12) { + level++; + if (len >= 16) { + level++; + } + } + + // decrease points + if ("abcdefghijklmnopqrstuvwxyz".indexOf(passwd) > 0 || "ABCDEFGHIJKLMNOPQRSTUVWXYZ".indexOf(passwd) > 0) { + level--; + } + if ("qwertyuiop".indexOf(passwd) > 0 || "asdfghjkl".indexOf(passwd) > 0 || "zxcvbnm".indexOf(passwd) > 0) { + level--; + } + if (StrUtil.isNumeric(passwd) && ("01234567890".indexOf(passwd) > 0 || "09876543210".indexOf(passwd) > 0)) { + level--; + } + + if (countLetter(passwd, CHAR_TYPE.NUM) == len || countLetter(passwd, CHAR_TYPE.SMALL_LETTER) == len + || countLetter(passwd, CHAR_TYPE.CAPITAL_LETTER) == len) { + level--; + } + + if (len % 2 == 0) { // aaabbb + String part1 = passwd.substring(0, len / 2); + String part2 = passwd.substring(len / 2); + if (part1.equals(part2)) { + level--; + } + if (StrUtil.isCharEquals(part1) && StrUtil.isCharEquals(part2)) { + level--; + } + } + if (len % 3 == 0) { // ababab + String part1 = passwd.substring(0, len / 3); + String part2 = passwd.substring(len / 3, len / 3 * 2); + String part3 = passwd.substring(len / 3 * 2); + if (part1.equals(part2) && part2.equals(part3)) { + level--; + } + } + + if (StrUtil.isNumeric(passwd) && len >= 6 && len <= 8) { // 19881010 or 881010 + int year = 0; + if (len == 8 || len == 6) { + year = Integer.parseInt(passwd.substring(0, len - 4)); + } + int size = sizeOfInt(year); + int month = Integer.parseInt(passwd.substring(size, size + 2)); + int day = Integer.parseInt(passwd.substring(size + 2, len)); + if (year >= 1950 && year < 2050 && month >= 1 && month <= 12 && day >= 1 && day <= 31) { + level--; + } + } + + for (String s : DICTIONARY) { + if (passwd.equals(s) || s.contains(passwd)) { + level--; + break; + } + } + + if (len <= 6) { + level--; + if (len <= 4) { + level--; + if (len <= 3) { + level = 0; + } + } + } + + if (StrUtil.isCharEquals(passwd)) { + level = 0; + } + + if (level < 0) { + level = 0; + } + + return level; + } + + /** + * Get password strength level, includes easy, midium, strong, very strong, extremely strong + * + * @param passwd 密码 + * @return 密码等级枚举 + */ + public static PASSWD_LEVEL getLevel(String passwd) { + int level = check(passwd); + switch (level) { + case 0: + case 1: + case 2: + case 3: + return PASSWD_LEVEL.EASY; + case 4: + case 5: + case 6: + return PASSWD_LEVEL.MIDIUM; + case 7: + case 8: + case 9: + return PASSWD_LEVEL.STRONG; + case 10: + case 11: + case 12: + return PASSWD_LEVEL.VERY_STRONG; + default: + return PASSWD_LEVEL.EXTREMELY_STRONG; + } + } + + /** + * Check character's type, includes num, capital letter, small letter and other character. + * 检查字符类型 + * + * @param c 字符 + * @return 类型 + */ + private static CHAR_TYPE checkCharacterType(char c) { + if (c >= 48 && c <= 57) { + return CHAR_TYPE.NUM; + } + if (c >= 65 && c <= 90) { + return CHAR_TYPE.CAPITAL_LETTER; + } + if (c >= 97 && c <= 122) { + return CHAR_TYPE.SMALL_LETTER; + } + return CHAR_TYPE.OTHER_CHAR; + } + + /** + * 计算密码中指定字符类型的数量 + * + * @param passwd 密码 + * @param type 类型 + * @return 数量 + */ + private static int countLetter(String passwd, CHAR_TYPE type) { + int count = 0; + if (null != passwd) { + final int length = passwd.length(); + if (length > 0) { + for (int i = 0; i < length; i++) { + if (checkCharacterType(passwd.charAt(i)) == type) { + count++; + } + } + } + } + return count; + } + + /** + * calculate the size of an integer number + * + * @param x 值 + * @return 数字长度 + */ + private static int sizeOfInt(int x) { + for (int i = 0; ; i++) + if (x <= SIZE_TABLE[i]) { + return i + 1; + } + } +} diff --git a/src/main/java/cn/hutool/core/text/Simhash.java b/src/main/java/cn/hutool/core/text/Simhash.java new file mode 100644 index 0000000..d84387a --- /dev/null +++ b/src/main/java/cn/hutool/core/text/Simhash.java @@ -0,0 +1,195 @@ +package cn.hutool.core.text; + +import cn.hutool.core.lang.hash.MurmurHash; + +import java.math.BigInteger; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.locks.StampedLock; + +/** + *

+ * Simhash是一种局部敏感hash,用于海量文本去重。
+ * 算法实现来自:https://github.com/xlturing/Simhash4J + *

+ * + *

+ * 局部敏感hash定义:假定两个字符串具有一定的相似性,在hash之后,仍然能保持这种相似性,就称之为局部敏感hash。 + *

+ * + * @author Looly, litaoxiao + * @since 4.3.3 + */ +public class Simhash { + + private final int bitNum = 64; + /** 存储段数,默认按照4段进行simhash存储 */ + private final int fracCount; + private final int fracBitNum; + /** 汉明距离的衡量标准,小于此距离标准表示相似 */ + private final int hammingThresh; + + /** 按照分段存储simhash,查找更快速 */ + private final List>> storage; + private final StampedLock lock = new StampedLock(); + + /** + * 构造 + */ + public Simhash() { + this(4, 3); + } + + /** + * 构造 + * + * @param fracCount 存储段数 + * @param hammingThresh 汉明距离的衡量标准 + */ + public Simhash(int fracCount, int hammingThresh) { + this.fracCount = fracCount; + this.fracBitNum = bitNum / fracCount; + this.hammingThresh = hammingThresh; + this.storage = new ArrayList<>(fracCount); + for (int i = 0; i < fracCount; i++) { + storage.add(new HashMap<>()); + } + } + + /** + * 指定文本计算simhash值 + * + * @param segList 分词的词列表 + * @return Hash值 + */ + public long hash(Collection segList) { + final int bitNum = this.bitNum; + // 按照词语的hash值,计算simHashWeight(低位对齐) + final int[] weight = new int[bitNum]; + long wordHash; + for (CharSequence seg : segList) { + wordHash = MurmurHash.hash64(seg); + for (int i = 0; i < bitNum; i++) { + if (((wordHash >> i) & 1) == 1) + weight[i] += 1; + else + weight[i] -= 1; + } + } + + // 计算得到Simhash值 + final StringBuilder sb = new StringBuilder(); + for (int i = 0; i < bitNum; i++) { + sb.append((weight[i] > 0) ? 1 : 0); + } + + return new BigInteger(sb.toString(), 2).longValue(); + } + + /** + * 判断文本是否与已存储的数据重复 + * + * @param segList 文本分词后的结果 + * @return 是否重复 + */ + public boolean equals(Collection segList) { + long simhash = hash(segList); + final List fracList = splitSimhash(simhash); + final int hammingThresh = this.hammingThresh; + + String frac; + Map> fracMap; + final long stamp = this.lock.readLock(); + try { + for (int i = 0; i < fracCount; i++) { + frac = fracList.get(i); + fracMap = storage.get(i); + if (fracMap.containsKey(frac)) { + for (Long simhash2 : fracMap.get(frac)) { + // 当汉明距离小于标准时相似 + if (hamming(simhash, simhash2) < hammingThresh) { + return true; + } + } + } + } + } finally { + this.lock.unlockRead(stamp); + } + return false; + } + + /** + * 按照(frac, 《simhash, content》)索引进行存储 + * + * @param simhash Simhash值 + */ + public void store(Long simhash) { + final int fracCount = this.fracCount; + final List>> storage = this.storage; + final List lFrac = splitSimhash(simhash); + + String frac; + Map> fracMap; + final long stamp = this.lock.writeLock(); + try { + for (int i = 0; i < fracCount; i++) { + frac = lFrac.get(i); + fracMap = storage.get(i); + if (fracMap.containsKey(frac)) { + fracMap.get(frac).add(simhash); + } else { + final List ls = new ArrayList<>(); + ls.add(simhash); + fracMap.put(frac, ls); + } + } + } finally { + this.lock.unlockWrite(stamp); + } + } + + //------------------------------------------------------------------------------------------------------ Private method start + /** + * 计算汉明距离 + * + * @param s1 值1 + * @param s2 值2 + * @return 汉明距离 + */ + private int hamming(Long s1, Long s2) { + final int bitNum = this.bitNum; + int dis = 0; + for (int i = 0; i < bitNum; i++) { + if ((s1 >> i & 1) != (s2 >> i & 1)) + dis++; + } + return dis; + } + + /** + * 将simhash分成n段 + * + * @param simhash Simhash值 + * @return N段Simhash + */ + private List splitSimhash(Long simhash) { + final int bitNum = this.bitNum; + final int fracBitNum = this.fracBitNum; + + final List ls = new ArrayList<>(); + final StringBuilder sb = new StringBuilder(); + for (int i = 0; i < bitNum; i++) { + sb.append(simhash >> i & 1); + if ((i + 1) % fracBitNum == 0) { + ls.add(sb.toString()); + sb.setLength(0); + } + } + return ls; + } + //------------------------------------------------------------------------------------------------------ Private method end +} diff --git a/src/main/java/cn/hutool/core/text/StrBuilder.java b/src/main/java/cn/hutool/core/text/StrBuilder.java new file mode 100644 index 0000000..18a18bd --- /dev/null +++ b/src/main/java/cn/hutool/core/text/StrBuilder.java @@ -0,0 +1,587 @@ +package cn.hutool.core.text; + +import cn.hutool.core.convert.Convert; +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.StrUtil; + +import java.io.Serializable; +import java.util.Arrays; + +/** + * 可复用的字符串生成器,非线程安全
+ * TODO 6.x移除此类,java8的StringBuilder非常完善了,无需重写。 + * + * @author Looly + * @since 4.0.0 + */ +public class StrBuilder implements CharSequence, Appendable, Serializable { + private static final long serialVersionUID = 6341229705927508451L; + + /** + * 默认容量 + */ + public static final int DEFAULT_CAPACITY = 16; + + /** + * 存放的字符数组 + */ + private char[] value; + /** + * 当前指针位置,或者叫做已经加入的字符数,此位置总在最后一个字符之后 + */ + private int position; + + /** + * 创建字符串构建器 + * + * @return this + */ + public static StrBuilder create() { + return new StrBuilder(); + } + + /** + * 创建字符串构建器 + * + * @param initialCapacity 初始容量 + * @return this + */ + public static StrBuilder create(int initialCapacity) { + return new StrBuilder(initialCapacity); + } + + /** + * 创建字符串构建器 + * + * @param strs 初始字符串 + * @return this + * @since 4.0.1 + */ + public static StrBuilder create(CharSequence... strs) { + return new StrBuilder(strs); + } + + // ------------------------------------------------------------------------------------ Constructor start + + /** + * 构造 + */ + public StrBuilder() { + this(DEFAULT_CAPACITY); + } + + /** + * 构造 + * + * @param initialCapacity 初始容量 + */ + public StrBuilder(int initialCapacity) { + value = new char[initialCapacity]; + } + + /** + * 构造 + * + * @param strs 初始字符串 + * @since 4.0.1 + */ + public StrBuilder(CharSequence... strs) { + this(ArrayUtil.isEmpty(strs) ? DEFAULT_CAPACITY : (totalLength(strs) + DEFAULT_CAPACITY)); + for (CharSequence str : strs) { + append(str); + } + } + // ------------------------------------------------------------------------------------ Constructor end + + // ------------------------------------------------------------------------------------ Append + + /** + * 追加对象,对象会被转换为字符串 + * + * @param obj 对象 + * @return this + */ + public StrBuilder append(Object obj) { + return insert(this.position, obj); + } + + /** + * 追加一个字符 + * + * @param c 字符 + * @return this + */ + @Override + public StrBuilder append(char c) { + return insert(this.position, c); + } + + /** + * 追加一个字符数组 + * + * @param src 字符数组 + * @return this + */ + public StrBuilder append(char[] src) { + if (ArrayUtil.isEmpty(src)) { + return this; + } + return append(src, 0, src.length); + } + + /** + * 追加一个字符数组 + * + * @param src 字符数组 + * @param srcPos 开始位置(包括) + * @param length 长度 + * @return this + */ + public StrBuilder append(char[] src, int srcPos, int length) { + return insert(this.position, src, srcPos, length); + } + + @Override + public StrBuilder append(CharSequence csq) { + return insert(this.position, csq); + } + + @Override + public StrBuilder append(CharSequence csq, int start, int end) { + return insert(this.position, csq, start, end); + } + + // ------------------------------------------------------------------------------------ Insert + + /** + * 追加对象,对象会被转换为字符串 + * + * @param index 插入位置 + * @param obj 对象 + * @return this + */ + public StrBuilder insert(int index, Object obj) { + if (obj instanceof CharSequence) { + return insert(index, (CharSequence) obj); + } + return insert(index, Convert.toStr(obj)); + } + + /** + * 插入指定字符 + * + * @param index 位置 + * @param c 字符 + * @return this + */ + public StrBuilder insert(int index, char c) { + if(index < 0){ + index = this.position + index; + } + if ((index < 0)) { + throw new StringIndexOutOfBoundsException(index); + } + + moveDataAfterIndex(index, 1); + value[index] = c; + this.position = Math.max(this.position, index) + 1; + return this; + } + + /** + * 指定位置插入数据
+ * 如果插入位置为当前位置,则定义为追加
+ * 如果插入位置大于当前位置,则中间部分补充空格 + * + * @param index 插入位置 + * @param src 源数组 + * @return this + */ + public StrBuilder insert(int index, char[] src) { + if (ArrayUtil.isEmpty(src)) { + return this; + } + return insert(index, src, 0, src.length); + } + + /** + * 指定位置插入数据
+ * 如果插入位置为当前位置,则定义为追加
+ * 如果插入位置大于当前位置,则中间部分补充空格 + * + * @param index 插入位置 + * @param src 源数组 + * @param srcPos 位置 + * @param length 长度 + * @return this + */ + public StrBuilder insert(int index, char[] src, int srcPos, int length) { + if (ArrayUtil.isEmpty(src) || srcPos > src.length || length <= 0) { + return this; + } + if(index < 0){ + index = this.position + index; + } + if ((index < 0)) { + throw new StringIndexOutOfBoundsException(index); + } + + if (srcPos < 0) { + srcPos = 0; + } else if (srcPos + length > src.length) { + // 长度越界,只截取最大长度 + length = src.length - srcPos; + } + + moveDataAfterIndex(index, length); + // 插入数据 + System.arraycopy(src, srcPos, value, index, length); + this.position = Math.max(this.position, index) + length; + return this; + } + + /** + * 指定位置插入字符串的某个部分
+ * 如果插入位置为当前位置,则定义为追加
+ * 如果插入位置大于当前位置,则中间部分补充空格 + * + * @param index 位置 + * @param csq 字符串 + * @return this + */ + public StrBuilder insert(int index, CharSequence csq) { + if(index < 0){ + index = this.position + index; + } + if ((index < 0)) { + throw new StringIndexOutOfBoundsException(index); + } + + if (null == csq) { + csq = StrUtil.EMPTY; + } + int len = csq.length(); + moveDataAfterIndex(index, csq.length()); + if (csq instanceof String) { + ((String) csq).getChars(0, len, this.value, index); + } else if (csq instanceof StringBuilder) { + ((StringBuilder) csq).getChars(0, len, this.value, index); + } else if (csq instanceof StringBuffer) { + ((StringBuffer) csq).getChars(0, len, this.value, index); + } else if (csq instanceof StrBuilder) { + ((StrBuilder) csq).getChars(0, len, this.value, index); + } else { + for (int i = 0, j = this.position; i < len; i++, j++) { + this.value[j] = csq.charAt(i); + } + } + this.position = Math.max(this.position, index) + len; + return this; + } + + /** + * 指定位置插入字符串的某个部分
+ * 如果插入位置为当前位置,则定义为追加
+ * 如果插入位置大于当前位置,则中间部分补充空格 + * + * @param index 位置 + * @param csq 字符串 + * @param start 字符串开始位置(包括) + * @param end 字符串结束位置(不包括) + * @return this + */ + public StrBuilder insert(int index, CharSequence csq, int start, int end) { + if (csq == null) { + csq = "null"; + } + final int csqLen = csq.length(); + if (start > csqLen) { + return this; + } + if (start < 0) { + start = 0; + } + if (end > csqLen) { + end = csqLen; + } + if (start >= end) { + return this; + } + if(index < 0){ + index = this.position + index; + } + if ((index < 0)) { + throw new StringIndexOutOfBoundsException(index); + } + + final int length = end - start; + moveDataAfterIndex(index, length); + for (int i = start, j = this.position; i < end; i++, j++) { + value[j] = csq.charAt(i); + } + this.position = Math.max(this.position, index) + length; + return this; + } + + // ------------------------------------------------------------------------------------ Others + + /** + * 将指定段的字符列表写出到目标字符数组中 + * + * @param srcBegin 起始位置(包括) + * @param srcEnd 结束位置(不包括) + * @param dst 目标数组 + * @param dstBegin 目标起始位置(包括) + * @return this + */ + public StrBuilder getChars(int srcBegin, int srcEnd, char[] dst, int dstBegin) { + if (srcBegin < 0) { + srcBegin = 0; + } + if (srcEnd < 0) { + srcEnd = 0; + } else if (srcEnd > this.position) { + srcEnd = this.position; + } + if (srcBegin > srcEnd) { + throw new StringIndexOutOfBoundsException("srcBegin > srcEnd"); + } + System.arraycopy(value, srcBegin, dst, dstBegin, srcEnd - srcBegin); + return this; + } + + /** + * 是否有内容 + * + * @return 是否有内容 + */ + public boolean hasContent() { + return position > 0; + } + + /** + * 是否为空 + * + * @return 是否为空 + */ + public boolean isEmpty() { + return position == 0; + } + + /** + * 删除全部字符,位置归零 + * + * @return this + */ + public StrBuilder clear() { + return reset(); + } + + /** + * 删除全部字符,位置归零 + * + * @return this + */ + public StrBuilder reset() { + this.position = 0; + return this; + } + + /** + * 删除到指定位置
+ * 如果新位置小于等于0,则删除全部 + * + * @param newPosition 新的位置,不包括这个位置 + * @return this + */ + public StrBuilder delTo(int newPosition) { + if (newPosition < 0) { + newPosition = 0; + } + return del(newPosition, this.position); + } + + /** + * 删除指定长度的字符,规则如下: + * + *
+	 * 1、end大于等于最大长度,结束按照最大长度计算,相当于删除start之后虽有部分(性能最好)
+	 * 2、end小于start时,抛出StringIndexOutOfBoundsException
+	 * 3、start小于0 按照0处理
+	 * 4、start等于end不处理
+	 * 5、start和end都位于长度区间内,删除这段内容(内存拷贝)
+	 * 
+ * + * @param start 开始位置,负数按照0处理(包括) + * @param end 结束位置,超出最大长度按照最大长度处理(不包括) + * @return this + * @throws StringIndexOutOfBoundsException 当start > end抛出此异常 + */ + public StrBuilder del(int start, int end) throws StringIndexOutOfBoundsException { + if (start < 0) { + start = 0; + } + + if (end >= this.position) { + // end在边界及以外,相当于删除后半部分 + this.position = start; + return this; + } else if (end < 0) { + // start和end都为0的情况下表示删除全部 + end = 0; + } + + int len = end - start; + // 截取中间部分,需要将后半部分复制到删除的开始位置 + if (len > 0) { + System.arraycopy(value, start + len, value, start, this.position - end); + this.position -= len; + } else if (len < 0) { + throw new StringIndexOutOfBoundsException("Start is greater than End."); + } + return this; + } + + /** + * 生成字符串 + * + * @param isReset 是否重置,重置后相当于空的构建器 + * @return 生成的字符串 + */ + public String toString(boolean isReset) { + if (position > 0) { + final String s = new String(value, 0, position); + if (isReset) { + reset(); + } + return s; + } + return StrUtil.EMPTY; + } + + /** + * 重置并返回生成的字符串 + * + * @return 字符串 + */ + public String toStringAndReset() { + return toString(true); + } + + /** + * 生成字符串 + */ + @Override + public String toString() { + return toString(false); + } + + @Override + public int length() { + return this.position; + } + + @Override + public char charAt(int index) { + if(index < 0){ + index = this.position + index; + } + if ((index < 0) || (index > this.position)) { + throw new StringIndexOutOfBoundsException(index); + } + return this.value[index]; + } + + @Override + public CharSequence subSequence(int start, int end) { + return subString(start, end); + } + + /** + * 返回自定段的字符串 + * + * @param start 开始位置(包括) + * @return this + */ + public String subString(int start) { + return subString(start, this.position); + } + + /** + * 返回自定段的字符串 + * + * @param start 开始位置(包括) + * @param end 结束位置(不包括) + * @return this + */ + public String subString(int start, int end) { + return new String(this.value, start, end - start); + } + + // ------------------------------------------------------------------------------------ Private method start + + /** + * 指定位置之后的数据后移指定长度 + * + * @param index 位置 + * @param length 位移长度 + */ + private void moveDataAfterIndex(int index, int length) { + ensureCapacity(Math.max(this.position, index) + length); + if (index < this.position) { + // 插入位置在已有数据范围内,后移插入位置之后的数据 + System.arraycopy(this.value, index, this.value, index + length, this.position - index); + } else if (index > this.position) { + // 插入位置超出范围,则当前位置到index清除为空格 + Arrays.fill(this.value, this.position, index, StrUtil.C_SPACE); + } + // 不位移 + } + + /** + * 确认容量是否够用,不够用则扩展容量 + * + * @param minimumCapacity 最小容量 + */ + private void ensureCapacity(int minimumCapacity) { + // overflow-conscious code + if (minimumCapacity - value.length > 0) { + expandCapacity(minimumCapacity); + } + } + + /** + * 扩展容量
+ * 首先对容量进行二倍扩展,如果小于最小容量,则扩展为最小容量 + * + * @param minimumCapacity 需要扩展的最小容量 + */ + private void expandCapacity(int minimumCapacity) { + int newCapacity = (value.length << 1) + 2; + // overflow-conscious code + if (newCapacity - minimumCapacity < 0) { + newCapacity = minimumCapacity; + } + if (newCapacity < 0) { + throw new OutOfMemoryError("Capacity is too long and max than Integer.MAX"); + } + value = Arrays.copyOf(value, newCapacity); + } + + /** + * 给定字符串数组的总长度
+ * null字符长度定义为0 + * + * @param strs 字符串数组 + * @return 总长度 + * @since 4.0.1 + */ + private static int totalLength(CharSequence... strs) { + int totalLength = 0; + for (CharSequence str : strs) { + totalLength += (null == str ? 0 : str.length()); + } + return totalLength; + } + // ------------------------------------------------------------------------------------ Private method end +} diff --git a/src/main/java/cn/hutool/core/text/StrFormatter.java b/src/main/java/cn/hutool/core/text/StrFormatter.java new file mode 100644 index 0000000..b90c23c --- /dev/null +++ b/src/main/java/cn/hutool/core/text/StrFormatter.java @@ -0,0 +1,126 @@ +package cn.hutool.core.text; + +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.StrUtil; + +import java.util.Map; + +/** + * 字符串格式化工具 + * + * @author Looly + */ +public class StrFormatter { + + /** + * 格式化字符串
+ * 此方法只是简单将占位符 {} 按照顺序替换为参数
+ * 如果想输出 {} 使用 \\转义 { 即可,如果想输出 {} 之前的 \ 使用双转义符 \\\\ 即可
+ * 例:
+ * 通常使用:format("this is {} for {}", "a", "b") =》 this is a for b
+ * 转义{}: format("this is \\{} for {}", "a", "b") =》 this is \{} for a
+ * 转义\: format("this is \\\\{} for {}", "a", "b") =》 this is \a for b
+ * + * @param strPattern 字符串模板 + * @param argArray 参数列表 + * @return 结果 + */ + public static String format(String strPattern, Object... argArray) { + return formatWith(strPattern, StrUtil.EMPTY_JSON, argArray); + } + + /** + * 格式化字符串
+ * 此方法只是简单将指定占位符 按照顺序替换为参数
+ * 如果想输出占位符使用 \\转义即可,如果想输出占位符之前的 \ 使用双转义符 \\\\ 即可
+ * 例:
+ * 通常使用:format("this is {} for {}", "{}", "a", "b") =》 this is a for b
+ * 转义{}: format("this is \\{} for {}", "{}", "a", "b") =》 this is {} for a
+ * 转义\: format("this is \\\\{} for {}", "{}", "a", "b") =》 this is \a for b
+ * + * @param strPattern 字符串模板 + * @param placeHolder 占位符,例如{} + * @param argArray 参数列表 + * @return 结果 + * @since 5.7.14 + */ + public static String formatWith(String strPattern, String placeHolder, Object... argArray) { + if (StrUtil.isBlank(strPattern) || StrUtil.isBlank(placeHolder) || ArrayUtil.isEmpty(argArray)) { + return strPattern; + } + final int strPatternLength = strPattern.length(); + final int placeHolderLength = placeHolder.length(); + + // 初始化定义好的长度以获得更好的性能 + final StringBuilder sbuf = new StringBuilder(strPatternLength + 50); + + int handledPosition = 0;// 记录已经处理到的位置 + int delimIndex;// 占位符所在位置 + for (int argIndex = 0; argIndex < argArray.length; argIndex++) { + delimIndex = strPattern.indexOf(placeHolder, handledPosition); + if (delimIndex == -1) {// 剩余部分无占位符 + if (handledPosition == 0) { // 不带占位符的模板直接返回 + return strPattern; + } + // 字符串模板剩余部分不再包含占位符,加入剩余部分后返回结果 + sbuf.append(strPattern, handledPosition, strPatternLength); + return sbuf.toString(); + } + + // 转义符 + if (delimIndex > 0 && strPattern.charAt(delimIndex - 1) == StrUtil.C_BACKSLASH) {// 转义符 + if (delimIndex > 1 && strPattern.charAt(delimIndex - 2) == StrUtil.C_BACKSLASH) {// 双转义符 + // 转义符之前还有一个转义符,占位符依旧有效 + sbuf.append(strPattern, handledPosition, delimIndex - 1); + sbuf.append(StrUtil.utf8Str(argArray[argIndex])); + handledPosition = delimIndex + placeHolderLength; + } else { + // 占位符被转义 + argIndex--; + sbuf.append(strPattern, handledPosition, delimIndex - 1); + sbuf.append(placeHolder.charAt(0)); + handledPosition = delimIndex + 1; + } + } else {// 正常占位符 + sbuf.append(strPattern, handledPosition, delimIndex); + sbuf.append(StrUtil.utf8Str(argArray[argIndex])); + handledPosition = delimIndex + placeHolderLength; + } + } + + // 加入最后一个占位符后所有的字符 + sbuf.append(strPattern, handledPosition, strPatternLength); + + return sbuf.toString(); + } + + /** + * 格式化文本,使用 {varName} 占位
+ * map = {a: "aValue", b: "bValue"} format("{a} and {b}", map) ---=》 aValue and bValue + * + * @param template 文本模板,被替换的部分用 {key} 表示 + * @param map 参数值对 + * @param ignoreNull 是否忽略 {@code null} 值,忽略则 {@code null} 值对应的变量不被替换,否则替换为"" + * @return 格式化后的文本 + * @since 5.7.10 + */ + public static String format(CharSequence template, Map map, boolean ignoreNull) { + if (null == template) { + return null; + } + if (null == map || map.isEmpty()) { + return template.toString(); + } + + String template2 = template.toString(); + String value; + for (Map.Entry entry : map.entrySet()) { + value = StrUtil.utf8Str(entry.getValue()); + if (null == value && ignoreNull) { + continue; + } + template2 = StrUtil.replace(template2, "{" + entry.getKey() + "}", value); + } + return template2; + } +} diff --git a/src/main/java/cn/hutool/core/text/StrJoiner.java b/src/main/java/cn/hutool/core/text/StrJoiner.java new file mode 100644 index 0000000..5c3bbf9 --- /dev/null +++ b/src/main/java/cn/hutool/core/text/StrJoiner.java @@ -0,0 +1,434 @@ +package cn.hutool.core.text; + +import cn.hutool.core.collection.ArrayIter; +import cn.hutool.core.collection.IterUtil; +import cn.hutool.core.io.IORuntimeException; +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; + +import java.io.IOException; +import java.io.Serializable; +import java.util.Iterator; +import java.util.function.Function; + +/** + * 字符串连接器(拼接器),通过给定的字符串和多个元素,拼接为一个字符串
+ * 相较于{@link java.util.StringJoiner}提供更加灵活的配置,包括: + *
    + *
  • 支持任意Appendable接口实现
  • + *
  • 支持每个元素单独wrap
  • + *
  • 支持自定义null的处理逻辑
  • + *
  • 支持自定义默认结果
  • + *
+ * + * @author looly + * @since 5.7.2 + */ +public class StrJoiner implements Appendable, Serializable { + private static final long serialVersionUID = 1L; + + private Appendable appendable; + private CharSequence delimiter; + private CharSequence prefix; + private CharSequence suffix; + // 前缀和后缀是否包装每个元素,true表示包装每个元素,false包装整个字符串 + private boolean wrapElement; + // null元素处理逻辑 + private NullMode nullMode = NullMode.NULL_STRING; + // 当结果为空时默认返回的拼接结果 + private String emptyResult = StrUtil.EMPTY; + + // appendable中是否包含内容,用于判断增加内容时,是否首先加入分隔符 + private boolean hasContent; + + /** + * 根据已有StrJoiner配置新建一个新的StrJoiner + * + * @param joiner 已有StrJoiner + * @return 新的StrJoiner,配置相同 + * @since 5.7.12 + */ + public static StrJoiner of(StrJoiner joiner) { + StrJoiner joinerNew = new StrJoiner(joiner.delimiter, joiner.prefix, joiner.suffix); + joinerNew.wrapElement = joiner.wrapElement; + joinerNew.nullMode = joiner.nullMode; + joinerNew.emptyResult = joiner.emptyResult; + + return joinerNew; + } + + /** + * 使用指定分隔符创建StrJoiner + * + * @param delimiter 分隔符 + * @return StrJoiner + */ + public static StrJoiner of(CharSequence delimiter) { + return new StrJoiner(delimiter); + } + + /** + * 使用指定分隔符创建StrJoiner + * + * @param delimiter 分隔符 + * @param prefix 前缀 + * @param suffix 后缀 + * @return StrJoiner + */ + public static StrJoiner of(CharSequence delimiter, CharSequence prefix, CharSequence suffix) { + return new StrJoiner(delimiter, prefix, suffix); + } + + /** + * 构造 + * + * @param delimiter 分隔符,{@code null}表示无连接符,直接拼接 + */ + public StrJoiner(CharSequence delimiter) { + this(null, delimiter); + } + + /** + * 构造 + * + * @param appendable 字符串追加器,拼接的字符串都将加入到此,{@code null}使用默认{@link StringBuilder} + * @param delimiter 分隔符,{@code null}表示无连接符,直接拼接 + */ + public StrJoiner(Appendable appendable, CharSequence delimiter) { + this(appendable, delimiter, null, null); + } + + /** + * 构造 + * + * @param delimiter 分隔符,{@code null}表示无连接符,直接拼接 + * @param prefix 前缀 + * @param suffix 后缀 + */ + public StrJoiner(CharSequence delimiter, CharSequence prefix, CharSequence suffix) { + this(null, delimiter, prefix, suffix); + } + + /** + * 构造 + * + * @param appendable 字符串追加器,拼接的字符串都将加入到此,{@code null}使用默认{@link StringBuilder} + * @param delimiter 分隔符,{@code null}表示无连接符,直接拼接 + * @param prefix 前缀 + * @param suffix 后缀 + */ + public StrJoiner(Appendable appendable, CharSequence delimiter, + CharSequence prefix, CharSequence suffix) { + if (null != appendable) { + this.appendable = appendable; + checkHasContent(appendable); + } + + this.delimiter = delimiter; + this.prefix = prefix; + this.suffix = suffix; + } + + /** + * 设置分隔符 + * + * @param delimiter 分隔符 + * @return this + */ + public StrJoiner setDelimiter(CharSequence delimiter) { + this.delimiter = delimiter; + return this; + } + + /** + * 设置前缀 + * + * @param prefix 前缀 + * @return this + */ + public StrJoiner setPrefix(CharSequence prefix) { + this.prefix = prefix; + return this; + } + + /** + * 设置后缀 + * + * @param suffix 后缀 + * @return this + */ + public StrJoiner setSuffix(CharSequence suffix) { + this.suffix = suffix; + return this; + } + + /** + * 设置前缀和后缀是否包装每个元素 + * + * @param wrapElement true表示包装每个元素,false包装整个字符串 + * @return this + */ + public StrJoiner setWrapElement(boolean wrapElement) { + this.wrapElement = wrapElement; + return this; + } + + /** + * 设置{@code null}元素处理逻辑 + * + * @param nullMode 逻辑枚举,可选忽略、转换为""或转换为null字符串 + * @return this + */ + public StrJoiner setNullMode(NullMode nullMode) { + this.nullMode = nullMode; + return this; + } + + /** + * 设置当没有任何元素加入时,默认返回的字符串,默认"" + * + * @param emptyResult 默认字符串 + * @return this + */ + public StrJoiner setEmptyResult(String emptyResult) { + this.emptyResult = emptyResult; + return this; + } + + /** + * 追加对象到拼接器中 + * + * @param obj 对象,支持数组、集合等 + * @return this + */ + public StrJoiner append(Object obj) { + if (null == obj) { + append((CharSequence) null); + } else if (ArrayUtil.isArray(obj)) { + append(new ArrayIter<>(obj)); + } else if (obj instanceof Iterator) { + append((Iterator) obj); + } else if (obj instanceof Iterable) { + append(((Iterable) obj).iterator()); + } else { + append(ObjectUtil.toString(obj)); + } + return this; + } + + /** + * 追加数组中的元素到拼接器中 + * + * @param 元素类型 + * @param array 元素数组 + * @return this + */ + public StrJoiner append(T[] array) { + if (null == array) { + return this; + } + return append(new ArrayIter<>(array)); + } + + /** + * 追加{@link Iterator}中的元素到拼接器中 + * + * @param 元素类型 + * @param iterator 元素列表 + * @return this + */ + public StrJoiner append(Iterator iterator) { + if (null != iterator) { + while (iterator.hasNext()) { + append(iterator.next()); + } + } + return this; + } + + /** + * 追加数组中的元素到拼接器中 + * + * @param 元素类型 + * @param array 元素数组 + * @param toStrFunc 元素对象转换为字符串的函数 + * @return this + */ + public StrJoiner append(T[] array, Function toStrFunc) { + return append((Iterator) new ArrayIter<>(array), toStrFunc); + } + + /** + * 追加{@link Iterator}中的元素到拼接器中 + * + * @param 元素类型 + * @param iterable 元素列表 + * @param toStrFunc 元素对象转换为字符串的函数 + * @return this + */ + public StrJoiner append(Iterable iterable, Function toStrFunc) { + return append(IterUtil.getIter(iterable), toStrFunc); + } + + /** + * 追加{@link Iterator}中的元素到拼接器中 + * + * @param 元素类型 + * @param iterator 元素列表 + * @param toStrFunc 元素对象转换为字符串的函数 + * @return this + */ + public StrJoiner append(Iterator iterator, Function toStrFunc) { + if (null != iterator) { + while (iterator.hasNext()) { + append(toStrFunc.apply(iterator.next())); + } + } + return this; + } + + @Override + public StrJoiner append(CharSequence csq) { + return append(csq, 0, StrUtil.length(csq)); + } + + @Override + public StrJoiner append(CharSequence csq, int startInclude, int endExclude) { + if (null == csq) { + switch (this.nullMode) { + case IGNORE: + return this; + case TO_EMPTY: + csq = StrUtil.EMPTY; + break; + case NULL_STRING: + csq = StrUtil.NULL; + endExclude = StrUtil.NULL.length(); + break; + } + } + try { + final Appendable appendable = prepare(); + if (wrapElement && StrUtil.isNotEmpty(this.prefix)) { + appendable.append(prefix); + } + appendable.append(csq, startInclude, endExclude); + if (wrapElement && StrUtil.isNotEmpty(this.suffix)) { + appendable.append(suffix); + } + } catch (IOException e) { + throw new IORuntimeException(e); + } + return this; + } + + @Override + public StrJoiner append(char c) { + return append(String.valueOf(c)); + } + + /** + * 合并一个StrJoiner 到当前的StrJoiner
+ * 合并规则为,在尾部直接追加,当存在{@link #prefix}时,如果{@link #wrapElement}为{@code false},则去除之。 + * + * @param strJoiner 其他的StrJoiner + * @return this + * @since 5.7.22 + */ + public StrJoiner merge(StrJoiner strJoiner){ + if(null != strJoiner && null != strJoiner.appendable){ + final String otherStr = strJoiner.toString(); + if(strJoiner.wrapElement){ + this.append(otherStr); + }else{ + this.append(otherStr, this.prefix.length(), otherStr.length()); + } + } + return this; + } + + /** + * 长度
+ * 长度计算方式为prefix + suffix + content
+ * 此方法结果与toString().length()一致。 + * + * @return 长度,如果结果为{@code null},返回-1 + * @since 5.7.22 + */ + public int length() { + return (this.appendable != null ? this.appendable.toString().length() + suffix.length() : + null == this.emptyResult ? -1 : emptyResult.length()); + } + + @Override + public String toString() { + if (null == this.appendable) { + return emptyResult; + } + + String result = this.appendable.toString(); + if (!wrapElement && StrUtil.isNotEmpty(this.suffix)) { + result += this.suffix; + } + return result; + } + + /** + * {@code null}处理的模式 + */ + public enum NullMode { + /** + * 忽略{@code null},即null元素不加入拼接的字符串 + */ + IGNORE, + /** + * {@code null}转为"" + */ + TO_EMPTY, + /** + * {@code null}转为null字符串 + */ + NULL_STRING + } + + /** + * 准备连接器,如果连接器非空,追加元素,否则初始化前缀 + * + * @return {@link Appendable} + * @throws IOException IO异常 + */ + private Appendable prepare() throws IOException { + if (hasContent) { + this.appendable.append(delimiter); + } else { + if (null == this.appendable) { + this.appendable = new StringBuilder(); + } + if (!wrapElement && StrUtil.isNotEmpty(this.prefix)) { + this.appendable.append(this.prefix); + } + this.hasContent = true; + } + return this.appendable; + } + + /** + * 检查用户传入的{@link Appendable} 是否已经存在内容,而且不能以分隔符结尾 + * + * @param appendable {@link Appendable} + */ + private void checkHasContent(Appendable appendable) { + if (appendable instanceof CharSequence) { + final CharSequence charSequence = (CharSequence) appendable; + if (charSequence.length() > 0 && StrUtil.endWith(charSequence, delimiter)) { + this.hasContent = true; + } + } else { + final String initStr = appendable.toString(); + if (StrUtil.isNotEmpty(initStr) && !StrUtil.endWith(initStr, delimiter)) { + this.hasContent = true; + } + } + } +} diff --git a/src/main/java/cn/hutool/core/text/StrMatcher.java b/src/main/java/cn/hutool/core/text/StrMatcher.java new file mode 100644 index 0000000..ccc59f8 --- /dev/null +++ b/src/main/java/cn/hutool/core/text/StrMatcher.java @@ -0,0 +1,119 @@ +package cn.hutool.core.text; + +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.StrUtil; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * 字符串模式匹配,使用${XXXXX}作为变量,例如: + * + *
+ *     pattern: ${name}-${age}-${gender}-${country}-${province}-${city}-${status}
+ *     text:    "小明-19-男-中国-河南-郑州-已婚"
+ *     result:  {name=小明, age=19, gender=男, country=中国, province=河南, city=郑州, status=已婚}
+ * 
+ * + * @author looly + * @since 5.6.0 + */ +public class StrMatcher { + + List patterns; + + /** + * 构造 + * + * @param pattern 模式,变量用${XXX}占位 + */ + public StrMatcher(String pattern) { + this.patterns = parse(pattern); + } + + /** + * 匹配并提取匹配到的内容 + * + * @param text 被匹配的文本 + * @return 匹配的map,key为变量名,value为匹配到的值 + */ + public Map match(String text) { + final HashMap result = MapUtil.newHashMap(true); + int from = 0; + String key = null; + int to; + for (String part : patterns) { + if (StrUtil.isWrap(part, "${", "}")) { + // 变量 + key = StrUtil.sub(part, 2, part.length() - 1); + } else { + to = text.indexOf(part, from); + if (to < 0) { + //普通字符串未匹配到,说明整个模式不能匹配,返回空 + return MapUtil.empty(); + } + if (null != key && to > from) { + // 变量对应部分有内容 + result.put(key, text.substring(from, to)); + } + // 下一个起始点是普通字符串的末尾 + from = to + part.length(); + key = null; + } + } + + if (null != key && from < text.length()) { + // 变量对应部分有内容 + result.put(key, text.substring(from)); + } + + return result; + } + + /** + * 解析表达式 + * + * @param pattern 表达式,使用${XXXX}作为变量占位符 + * @return 表达式 + */ + private static List parse(String pattern) { + List patterns = new ArrayList<>(); + final int length = pattern.length(); + char c = 0; + char pre; + boolean inVar = false; + StringBuilder part = StrUtil.builder(); + for (int i = 0; i < length; i++) { + pre = c; + c = pattern.charAt(i); + if (inVar) { + part.append(c); + if ('}' == c) { + // 变量结束 + inVar = false; + patterns.add(part.toString()); + part.setLength(0); + } + } else if ('{' == c && '$' == pre) { + // 变量开始 + inVar = true; + final String preText = part.substring(0, part.length() - 1); + if (StrUtil.isNotEmpty(preText)) { + patterns.add(preText); + } + part.setLength(0); + part.append(pre).append(c); + } else { + // 普通字符 + part.append(c); + } + } + + if (part.length() > 0) { + patterns.add(part.toString()); + } + return patterns; + } +} diff --git a/src/main/java/cn/hutool/core/text/StrPool.java b/src/main/java/cn/hutool/core/text/StrPool.java new file mode 100644 index 0000000..b78c666 --- /dev/null +++ b/src/main/java/cn/hutool/core/text/StrPool.java @@ -0,0 +1,180 @@ +package cn.hutool.core.text; + +/** + * 常用字符串常量定义 + * @see CharPool + * + * @author looly + * @since 5.6.3 + */ +public interface StrPool { + + /** + * 字符常量:空格符 {@code ' '} + */ + char C_SPACE = CharPool.SPACE; + + /** + * 字符常量:制表符 {@code '\t'} + */ + char C_TAB = CharPool.TAB; + + /** + * 字符常量:点 {@code '.'} + */ + char C_DOT = CharPool.DOT; + + /** + * 字符常量:斜杠 {@code '/'} + */ + char C_SLASH = CharPool.SLASH; + + /** + * 字符常量:反斜杠 {@code '\\'} + */ + char C_BACKSLASH = CharPool.BACKSLASH; + + /** + * 字符常量:回车符 {@code '\r'} + */ + char C_CR = CharPool.CR; + + /** + * 字符常量:换行符 {@code '\n'} + */ + char C_LF = CharPool.LF; + + /** + * 字符常量:下划线 {@code '_'} + */ + char C_UNDERLINE = CharPool.UNDERLINE; + + /** + * 字符常量:逗号 {@code ','} + */ + char C_COMMA = CharPool.COMMA; + + /** + * 字符常量:花括号(左) '{' + */ + char C_DELIM_START = CharPool.DELIM_START; + + /** + * 字符常量:花括号(右) '}' + */ + char C_DELIM_END = CharPool.DELIM_END; + + /** + * 字符常量:中括号(左) {@code '['} + */ + char C_BRACKET_START = CharPool.BRACKET_START; + + /** + * 字符常量:中括号(右) {@code ']'} + */ + char C_BRACKET_END = CharPool.BRACKET_END; + + /** + * 字符常量:冒号 {@code ':'} + */ + char C_COLON = CharPool.COLON; + + /** + * 字符常量:艾特 {@code '@'} + */ + char C_AT = CharPool.AT; + + /** + * 字符串常量:制表符 {@code "\t"} + */ + String TAB = " "; + + /** + * 字符串常量:点 {@code "."} + */ + String DOT = "."; + + /** + * 字符串常量:双点 {@code ".."}
+ * 用途:作为指向上级文件夹的路径,如:{@code "../path"} + */ + String DOUBLE_DOT = ".."; + + /** + * 字符串常量:斜杠 {@code "/"} + */ + String SLASH = "/"; + + /** + * 字符串常量:反斜杠 {@code "\\"} + */ + String BACKSLASH = "\\"; + + /** + * 字符串常量:回车符 {@code "\r"}
+ * 解释:该字符常用于表示 Linux 系统和 MacOS 系统下的文本换行 + */ + String CR = "\r"; + + /** + * 字符串常量:换行符 {@code "\n"} + */ + String LF = "\n"; + + /** + * 字符串常量:Windows 换行 {@code "\r\n"}
+ * 解释:该字符串常用于表示 Windows 系统下的文本换行 + */ + String CRLF = "\r\n"; + + /** + * 字符串常量:下划线 {@code "_"} + */ + String UNDERLINE = "_"; + + /** + * 字符串常量:减号(连接符) {@code "-"} + */ + String DASHED = "-"; + + /** + * 字符串常量:逗号 {@code ","} + */ + String COMMA = ","; + + /** + * 字符串常量:花括号(左) "{" + */ + String DELIM_START = "{"; + + /** + * 字符串常量:花括号(右) "}" + */ + String DELIM_END = "}"; + + /** + * 字符串常量:中括号(左) {@code "["} + */ + String BRACKET_START = "["; + + /** + * 字符串常量:中括号(右) {@code "]"} + */ + String BRACKET_END = "]"; + + /** + * 字符串常量:冒号 {@code ":"} + */ + String COLON = ":"; + + /** + * 字符串常量:艾特 {@code "@"} + */ + String AT = "@"; + + + /** + * 字符串常量:空 JSON {@code "{}"} + */ + String EMPTY_JSON = "{}"; +} diff --git a/src/main/java/cn/hutool/core/text/StrSplitter.java b/src/main/java/cn/hutool/core/text/StrSplitter.java new file mode 100644 index 0000000..ce9d998 --- /dev/null +++ b/src/main/java/cn/hutool/core/text/StrSplitter.java @@ -0,0 +1,457 @@ +package cn.hutool.core.text; + +import cn.hutool.core.lang.PatternPool; +import cn.hutool.core.text.finder.CharFinder; +import cn.hutool.core.text.finder.CharMatcherFinder; +import cn.hutool.core.text.finder.LengthFinder; +import cn.hutool.core.text.finder.PatternFinder; +import cn.hutool.core.text.finder.StrFinder; +import cn.hutool.core.text.split.SplitIter; +import cn.hutool.core.util.CharUtil; +import cn.hutool.core.util.StrUtil; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Function; +import java.util.regex.Pattern; + +/** + * 字符串切分器,封装统一的字符串分割静态方法 + * @author Looly + * @since 5.7.0 + */ +public class StrSplitter { + + //---------------------------------------------------------------------------------------------- Split by char + + /** + * 切分字符串路径,仅支持Unix分界符:/ + * + * @param str 被切分的字符串 + * @return 切分后的集合 + * @since 3.0.8 + */ + public static List splitPath(CharSequence str) { + return splitPath(str, 0); + } + + /** + * 切分字符串路径,仅支持Unix分界符:/ + * + * @param str 被切分的字符串 + * @return 切分后的集合 + * @since 3.0.8 + */ + public static String[] splitPathToArray(CharSequence str) { + return toArray(splitPath(str)); + } + + /** + * 切分字符串路径,仅支持Unix分界符:/ + * + * @param str 被切分的字符串 + * @param limit 限制分片数 + * @return 切分后的集合 + * @since 3.0.8 + */ + public static List splitPath(CharSequence str, int limit) { + return split(str, StrUtil.C_SLASH, limit, true, true); + } + + /** + * 切分字符串路径,仅支持Unix分界符:/ + * + * @param str 被切分的字符串 + * @param limit 限制分片数 + * @return 切分后的集合 + * @since 3.0.8 + */ + public static String[] splitPathToArray(CharSequence str, int limit) { + return toArray(splitPath(str, limit)); + } + + /** + * 切分字符串 + * + * @param str 被切分的字符串 + * @param separator 分隔符字符 + * @param ignoreEmpty 是否忽略空串 + * @return 切分后的集合 + * @since 3.2.1 + */ + public static List splitTrim(CharSequence str, char separator, boolean ignoreEmpty) { + return split(str, separator, 0, true, ignoreEmpty); + } + + /** + * 切分字符串 + * + * @param str 被切分的字符串 + * @param separator 分隔符字符 + * @param isTrim 是否去除切分字符串后每个元素两边的空格 + * @param ignoreEmpty 是否忽略空串 + * @return 切分后的集合 + * @since 3.0.8 + */ + public static List split(CharSequence str, char separator, boolean isTrim, boolean ignoreEmpty) { + return split(str, separator, 0, isTrim, ignoreEmpty); + } + + /** + * 切分字符串,大小写敏感,去除每个元素两边空白符 + * + * @param str 被切分的字符串 + * @param separator 分隔符字符 + * @param limit 限制分片数,-1不限制 + * @param ignoreEmpty 是否忽略空串 + * @return 切分后的集合 + * @since 3.0.8 + */ + public static List splitTrim(CharSequence str, char separator, int limit, boolean ignoreEmpty) { + return split(str, separator, limit, true, ignoreEmpty, false); + } + + /** + * 切分字符串,大小写敏感 + * + * @param str 被切分的字符串 + * @param separator 分隔符字符 + * @param limit 限制分片数,-1不限制 + * @param isTrim 是否去除切分字符串后每个元素两边的空格 + * @param ignoreEmpty 是否忽略空串 + * @return 切分后的集合 + * @since 3.0.8 + */ + public static List split(CharSequence str, char separator, int limit, boolean isTrim, boolean ignoreEmpty) { + return split(str, separator, limit, isTrim, ignoreEmpty, false); + } + + /** + * 切分字符串,大小写敏感 + * + * @param 切分后的元素类型 + * @param str 被切分的字符串 + * @param separator 分隔符字符 + * @param limit 限制分片数,-1不限制 + * @param ignoreEmpty 是否忽略空串 + * @param mapping 切分后的字符串元素的转换方法 + * @return 切分后的集合,元素类型是经过 mapping 转换后的 + * @since 5.7.14 + */ + public static List split(CharSequence str, char separator, int limit, boolean ignoreEmpty, Function mapping) { + return split(str, separator, limit, ignoreEmpty, false, mapping); + } + + /** + * 切分字符串,忽略大小写 + * + * @param text 被切分的字符串 + * @param separator 分隔符字符 + * @param limit 限制分片数,-1不限制 + * @param isTrim 是否去除切分字符串后每个元素两边的空格 + * @param ignoreEmpty 是否忽略空串 + * @return 切分后的集合 + * @since 3.2.1 + */ + public static List splitIgnoreCase(CharSequence text, char separator, int limit, boolean isTrim, boolean ignoreEmpty) { + return split(text, separator, limit, isTrim, ignoreEmpty, true); + } + + /** + * 切分字符串 + * + * @param text 被切分的字符串 + * @param separator 分隔符字符 + * @param limit 限制分片数,-1不限制 + * @param isTrim 是否去除切分字符串后每个元素两边的空格 + * @param ignoreEmpty 是否忽略空串 + * @param ignoreCase 是否忽略大小写 + * @return 切分后的集合 + */ + public static List split(CharSequence text, char separator, int limit, boolean isTrim, boolean ignoreEmpty, boolean ignoreCase) { + return split(text, separator, limit, ignoreEmpty, ignoreCase, trimFunc(isTrim)); + } + + /** + * 切分字符串
+ * 如果为空字符串或者null 则返回空集合 + * + * @param 切分后的元素类型 + * @param text 被切分的字符串 + * @param separator 分隔符字符 + * @param limit 限制分片数,-1不限制 + * @param ignoreEmpty 是否忽略空串 + * @param ignoreCase 是否忽略大小写 + * @param mapping 切分后的字符串元素的转换方法 + * @return 切分后的集合,元素类型是经过 mapping 转换后的 + * @since 5.7.14 + */ + public static List split(CharSequence text, char separator, int limit, boolean ignoreEmpty, + boolean ignoreCase, Function mapping) { + if (null == text) { + return new ArrayList<>(0); + } + final SplitIter splitIter = new SplitIter(text, new CharFinder(separator, ignoreCase), limit, ignoreEmpty); + return splitIter.toList(mapping); + } + + /** + * 切分字符串为字符串数组 + * + * @param str 被切分的字符串 + * @param separator 分隔符字符 + * @param limit 限制分片数 + * @param isTrim 是否去除切分字符串后每个元素两边的空格 + * @param ignoreEmpty 是否忽略空串 + * @return 切分后的集合 + * @since 3.0.8 + */ + public static String[] splitToArray(CharSequence str, char separator, int limit, boolean isTrim, boolean ignoreEmpty) { + return toArray(split(str, separator, limit, isTrim, ignoreEmpty)); + } + + //---------------------------------------------------------------------------------------------- Split by String + + /** + * 切分字符串,不忽略大小写 + * + * @param str 被切分的字符串 + * @param separator 分隔符字符串 + * @param isTrim 是否去除切分字符串后每个元素两边的空格 + * @param ignoreEmpty 是否忽略空串 + * @return 切分后的集合 + * @since 3.0.8 + */ + public static List split(CharSequence str, String separator, boolean isTrim, boolean ignoreEmpty) { + return split(str, separator, -1, isTrim, ignoreEmpty, false); + } + + /** + * 切分字符串,去除每个元素两边空格,忽略大小写 + * + * @param str 被切分的字符串 + * @param separator 分隔符字符串 + * @param ignoreEmpty 是否忽略空串 + * @return 切分后的集合 + * @since 3.2.1 + */ + public static List splitTrim(CharSequence str, String separator, boolean ignoreEmpty) { + return split(str, separator, true, ignoreEmpty); + } + + /** + * 切分字符串,不忽略大小写 + * + * @param str 被切分的字符串 + * @param separator 分隔符字符串 + * @param limit 限制分片数,小于等于0表示无限制 + * @param isTrim 是否去除切分字符串后每个元素两边的空格 + * @param ignoreEmpty 是否忽略空串 + * @return 切分后的集合 + * @since 3.0.8 + */ + public static List split(CharSequence str, String separator, int limit, boolean isTrim, boolean ignoreEmpty) { + return split(str, separator, limit, isTrim, ignoreEmpty, false); + } + + /** + * 切分字符串,去除每个元素两边空格,忽略大小写 + * + * @param str 被切分的字符串 + * @param separator 分隔符字符串 + * @param limit 限制分片数 + * @param ignoreEmpty 是否忽略空串 + * @return 切分后的集合 + * @since 3.2.1 + */ + public static List splitTrim(CharSequence str, String separator, int limit, boolean ignoreEmpty) { + return split(str, separator, limit, true, ignoreEmpty); + } + + /** + * 切分字符串,忽略大小写 + * + * @param str 被切分的字符串 + * @param separator 分隔符字符串 + * @param limit 限制分片数 + * @param isTrim 是否去除切分字符串后每个元素两边的空格 + * @param ignoreEmpty 是否忽略空串 + * @return 切分后的集合 + * @since 3.2.1 + */ + public static List splitIgnoreCase(CharSequence str, String separator, int limit, boolean isTrim, boolean ignoreEmpty) { + return split(str, separator, limit, isTrim, ignoreEmpty, true); + } + + /** + * 切分字符串,去除每个元素两边空格,忽略大小写 + * + * @param str 被切分的字符串 + * @param separator 分隔符字符串 + * @param limit 限制分片数 + * @param ignoreEmpty 是否忽略空串 + * @return 切分后的集合 + * @since 3.2.1 + */ + public static List splitTrimIgnoreCase(CharSequence str, String separator, int limit, boolean ignoreEmpty) { + return split(str, separator, limit, true, ignoreEmpty, true); + } + + /** + * 切分字符串
+ * 如果为空字符串或者null 则返回空集合 + * + * @param text 被切分的字符串 + * @param separator 分隔符字符串 + * @param limit 限制分片数,小于等于0表示无限制 + * @param isTrim 是否去除切分字符串后每个元素两边的空格 + * @param ignoreEmpty 是否忽略空串 + * @param ignoreCase 是否忽略大小写 + * @return 切分后的集合 + * @since 3.2.1 + */ + public static List split(CharSequence text, String separator, int limit, boolean isTrim, boolean ignoreEmpty, boolean ignoreCase) { + if (null == text) { + return new ArrayList<>(0); + } + final SplitIter splitIter = new SplitIter(text, new StrFinder(separator, ignoreCase), limit, ignoreEmpty); + return splitIter.toList(isTrim); + } + + /** + * 切分字符串为字符串数组 + * + * @param str 被切分的字符串 + * @param separator 分隔符字符 + * @param limit 限制分片数,小于等于0表示无限制 + * @param isTrim 是否去除切分字符串后每个元素两边的空格 + * @param ignoreEmpty 是否忽略空串 + * @return 切分后的集合 + * @since 3.0.8 + */ + public static String[] splitToArray(CharSequence str, String separator, int limit, boolean isTrim, boolean ignoreEmpty) { + return toArray(split(str, separator, limit, isTrim, ignoreEmpty)); + } + + //---------------------------------------------------------------------------------------------- Split by Whitespace + + /** + * 使用空白符切分字符串
+ * 切分后的字符串两边不包含空白符,空串或空白符串并不做为元素之一
+ * 如果为空字符串或者null 则返回空集合 + * + * @param text 被切分的字符串 + * @param limit 限制分片数 + * @return 切分后的集合 + * @since 3.0.8 + */ + public static List split(CharSequence text, int limit) { + if (null == text) { + return new ArrayList<>(0); + } + final SplitIter splitIter = new SplitIter(text, new CharMatcherFinder(CharUtil::isBlankChar), limit, true); + return splitIter.toList(false); + } + + /** + * 切分字符串为字符串数组 + * + * @param str 被切分的字符串 + * @param limit 限制分片数 + * @return 切分后的集合 + * @since 3.0.8 + */ + public static String[] splitToArray(String str, int limit) { + return toArray(split(str, limit)); + } + //---------------------------------------------------------------------------------------------- Split by regex + + /** + * 通过正则切分字符串 + * + * @param text 字符串 + * @param separatorRegex 分隔符正则 + * @param limit 限制分片数 + * @param isTrim 是否去除切分字符串后每个元素两边的空格 + * @param ignoreEmpty 是否忽略空串 + * @return 切分后的集合 + * @since 3.0.8 + */ + public static List splitByRegex(String text, String separatorRegex, int limit, boolean isTrim, boolean ignoreEmpty) { + final Pattern pattern = PatternPool.get(separatorRegex); + return split(text, pattern, limit, isTrim, ignoreEmpty); + } + + /** + * 通过正则切分字符串
+ * 如果为空字符串或者null 则返回空集合 + * + * @param text 字符串 + * @param separatorPattern 分隔符正则{@link Pattern} + * @param limit 限制分片数 + * @param isTrim 是否去除切分字符串后每个元素两边的空格 + * @param ignoreEmpty 是否忽略空串 + * @return 切分后的集合 + * @since 3.0.8 + */ + public static List split(String text, Pattern separatorPattern, int limit, boolean isTrim, boolean ignoreEmpty) { + if (null == text) { + return new ArrayList<>(0); + } + final SplitIter splitIter = new SplitIter(text, new PatternFinder(separatorPattern), limit, ignoreEmpty); + return splitIter.toList(isTrim); + } + + /** + * 通过正则切分字符串为字符串数组 + * + * @param str 被切分的字符串 + * @param separatorPattern 分隔符正则{@link Pattern} + * @param limit 限制分片数 + * @param isTrim 是否去除切分字符串后每个元素两边的空格 + * @param ignoreEmpty 是否忽略空串 + * @return 切分后的集合 + * @since 3.0.8 + */ + public static String[] splitToArray(String str, Pattern separatorPattern, int limit, boolean isTrim, boolean ignoreEmpty) { + return toArray(split(str, separatorPattern, limit, isTrim, ignoreEmpty)); + } + //---------------------------------------------------------------------------------------------- Split by length + + /** + * 根据给定长度,将给定字符串截取为多个部分 + * + * @param text 字符串 + * @param len 每一个小节的长度 + * @return 截取后的字符串数组 + */ + public static String[] splitByLength(CharSequence text, int len) { + if (null == text) { + return new String[0]; + } + SplitIter splitIter = new SplitIter(text, new LengthFinder(len), -1, false); + return splitIter.toArray(false); + } + //---------------------------------------------------------------------------------------------------------- Private method start + + /** + * List转Array + * + * @param list List + * @return Array + */ + private static String[] toArray(List list) { + return list.toArray(new String[0]); + } + + /** + * Trim函数 + * + * @param isTrim 是否trim + * @return {@link Function} + */ + private static Function trimFunc(boolean isTrim) { + return (str) -> isTrim ? StrUtil.trim(str) : str; + } + //---------------------------------------------------------------------------------------------------------- Private method end +} diff --git a/src/main/java/cn/hutool/core/text/TextSimilarity.java b/src/main/java/cn/hutool/core/text/TextSimilarity.java new file mode 100644 index 0000000..2401d2f --- /dev/null +++ b/src/main/java/cn/hutool/core/text/TextSimilarity.java @@ -0,0 +1,169 @@ +package cn.hutool.core.text; + +import cn.hutool.core.util.NumberUtil; +import cn.hutool.core.util.StrUtil; + +/** + * 文本相似度计算
+ * 工具类提供者:【杭州】fineliving + * + * @author fanqun + * @since 3.2.3 + **/ +public class TextSimilarity { + + /** + * 利用莱文斯坦距离(Levenshtein distance)算法计算相似度,两个都是空串相似度为1,被认为是相同的串
+ * 比较方法为: + *
    + *
  • 只比较两个字符串字母、数字、汉字部分,其他符号去除
  • + *
  • 计算出两个字符串最大子串,除以最长的字符串,结果即为相似度
  • + *
+ * + * @param strA 字符串1 + * @param strB 字符串2 + * @return 相似度 + */ + public static double similar(String strA, String strB) { + String newStrA, newStrB; + if (strA.length() < strB.length()) { + newStrA = removeSign(strB); + newStrB = removeSign(strA); + } else { + newStrA = removeSign(strA); + newStrB = removeSign(strB); + } + + // 用较大的字符串长度作为分母,相似子串作为分子计算出字串相似度 + int temp = Math.max(newStrA.length(), newStrB.length()); + if(0 == temp) { + // 两个都是空串相似度为1,被认为是相同的串 + return 1; + } + + final int commonLength = longestCommonSubstringLength(newStrA, newStrB); + return NumberUtil.div(commonLength, temp); + } + + /** + * 利用莱文斯坦距离(Levenshtein distance)算法计算相似度百分比 + * + * @param strA 字符串1 + * @param strB 字符串2 + * @param scale 保留小数 + * @return 百分比 + */ + public static String similar(String strA, String strB, int scale) { + return NumberUtil.formatPercent(similar(strA, strB), scale); + } + + /** + * 最长公共子串,采用动态规划算法。 其不要求所求得的字符在所给的字符串中是连续的。
+ * 算法解析见:https://leetcode-cn.com/problems/longest-common-subsequence/solution/zui-chang-gong-gong-zi-xu-lie-by-leetcod-y7u0/ + * + * @param strA 字符串1 + * @param strB 字符串2 + * @return 最长公共子串 + */ + public static String longestCommonSubstring(String strA, String strB) { + // 初始化矩阵数据,matrix[0][0]的值为0, 如果字符数组chars_strA和chars_strB的对应位相同,则matrix[i][j]的值为左上角的值加1, + // 否则,matrix[i][j]的值等于左上方最近两个位置的较大值, 矩阵中其余各点的值为0. + final int[][] matrix = generateMatrix(strA, strB); + + int m = strA.length(); + int n = strB.length(); + // 矩阵中,如果matrix[m][n]的值不等于matrix[m-1][n]的值也不等于matrix[m][n-1]的值, + // 则matrix[m][n]对应的字符为相似字符元,并将其存入result数组中。 + char[] result = new char[matrix[m][n]]; + int currentIndex = result.length - 1; + while (matrix[m][n] != 0) { + if (matrix[m][n] == matrix[m][n - 1]) { + n--; + } else if (matrix[m][n] == matrix[m - 1][n]) { + m--; + } else { + result[currentIndex] = strA.charAt(m - 1); + currentIndex--; + n--; + m--; + } + } + return new String(result); + } + + // --------------------------------------------------------------------------------------------------- Private method start + /** + * 将字符串的所有数据依次写成一行,去除无意义字符串 + * + * @param str 字符串 + * @return 处理后的字符串 + */ + private static String removeSign(String str) { + int length = str.length(); + StringBuilder sb = StrUtil.builder(length); + // 遍历字符串str,如果是汉字数字或字母,则追加到ab上面 + char c; + for (int i = 0; i < length; i++) { + c = str.charAt(i); + if(isValidChar(c)) { + sb.append(c); + } + } + + return sb.toString(); + } + + /** + * 判断字符是否为汉字,数字和字母, 因为对符号进行相似度比较没有实际意义,故符号不加入考虑范围。 + * + * @param charValue 字符 + * @return true表示为非汉字,数字和字母,false反之 + */ + private static boolean isValidChar(char charValue) { + return (charValue >= 0x4E00 && charValue <= 0X9FFF) || // + (charValue >= 'a' && charValue <= 'z') || // + (charValue >= 'A' && charValue <= 'Z') || // + (charValue >= '0' && charValue <= '9'); + } + + /** + * 求公共子串,采用动态规划算法。 其不要求所求得的字符在所给的字符串中是连续的。 + * + * @param strA 字符串1 + * @param strB 字符串2 + * @return 公共子串 + */ + private static int longestCommonSubstringLength(String strA, String strB) { + final int m = strA.length(); + final int n = strB.length(); + return generateMatrix(strA, strB)[m][n]; + } + + /** + * 求公共子串,采用动态规划算法。 其不要求所求得的字符在所给的字符串中是连续的。 + * + * @param strA 字符串1 + * @param strB 字符串2 + * @return 公共串矩阵 + */ + private static int[][] generateMatrix(String strA, String strB) { + int m = strA.length(); + int n = strB.length(); + + // 初始化矩阵数据,matrix[0][0]的值为0, 如果字符数组chars_strA和chars_strB的对应位相同,则matrix[i][j]的值为左上角的值加1, + // 否则,matrix[i][j]的值等于左上方最近两个位置的较大值, 矩阵中其余各点的值为0. + final int[][] matrix = new int[m + 1][n + 1]; + for (int i = 1; i <= m; i++) { + for (int j = 1; j <= n; j++) { + if (strA.charAt(i - 1) == strB.charAt(j - 1)) { + matrix[i][j] = matrix[i - 1][j - 1] + 1; + } else { + matrix[i][j] = Math.max(matrix[i][j - 1], matrix[i - 1][j]); + } + } + } + + return matrix; + } + // --------------------------------------------------------------------------------------------------- Private method end +} diff --git a/src/main/java/cn/hutool/core/text/UnicodeUtil.java b/src/main/java/cn/hutool/core/text/UnicodeUtil.java new file mode 100644 index 0000000..e0789e6 --- /dev/null +++ b/src/main/java/cn/hutool/core/text/UnicodeUtil.java @@ -0,0 +1,116 @@ +package cn.hutool.core.text; + +import cn.hutool.core.util.CharUtil; +import cn.hutool.core.util.HexUtil; +import cn.hutool.core.util.StrUtil; + +/** + * 提供Unicode字符串和普通字符串之间的转换 + * + * @author 兜兜毛毛, looly + * @since 4.0.0 + */ +public class UnicodeUtil { + + /** + * Unicode字符串转为普通字符串
+ * Unicode字符串的表现方式为:\\uXXXX + * + * @param unicode Unicode字符串 + * @return 普通字符串 + */ + public static String toString(String unicode) { + if (StrUtil.isBlank(unicode)) { + return unicode; + } + + final int len = unicode.length(); + StringBuilder sb = new StringBuilder(len); + int i; + int pos = 0; + while ((i = StrUtil.indexOfIgnoreCase(unicode, "\\u", pos)) != -1) { + sb.append(unicode, pos, i);//写入Unicode符之前的部分 + pos = i; + if (i + 5 < len) { + char c; + try { + c = (char) Integer.parseInt(unicode.substring(i + 2, i + 6), 16); + sb.append(c); + pos = i + 6;//跳过整个Unicode符 + } catch (NumberFormatException e) { + //非法Unicode符,跳过 + sb.append(unicode, pos, i + 2);//写入"\\u" + pos = i + 2; + } + } else { + //非Unicode符,结束 + break; + } + } + + if (pos < len) { + sb.append(unicode, pos, len); + } + return sb.toString(); + } + + /** + * 字符编码为Unicode形式 + * + * @param c 被编码的字符 + * @return Unicode字符串 + * @since 5.6.2 + * @see HexUtil#toUnicodeHex(char) + */ + public static String toUnicode(char c) { + return HexUtil.toUnicodeHex(c); + } + + /** + * 字符编码为Unicode形式 + * + * @param c 被编码的字符 + * @return Unicode字符串 + * @since 5.6.2 + * @see HexUtil#toUnicodeHex(int) + */ + public static String toUnicode(int c) { + return HexUtil.toUnicodeHex(c); + } + + /** + * 字符串编码为Unicode形式 + * + * @param str 被编码的字符串 + * @return Unicode字符串 + */ + public static String toUnicode(String str) { + return toUnicode(str, true); + } + + /** + * 字符串编码为Unicode形式 + * + * @param str 被编码的字符串 + * @param isSkipAscii 是否跳过ASCII字符(只跳过可见字符) + * @return Unicode字符串 + */ + public static String toUnicode(String str, boolean isSkipAscii) { + if (StrUtil.isEmpty(str)) { + return str; + } + + final int len = str.length(); + final StringBuilder unicode = new StringBuilder(str.length() * 6); + char c; + for (int i = 0; i < len; i++) { + c = str.charAt(i); + if (isSkipAscii && CharUtil.isAsciiPrintable(c)) { + unicode.append(c); + } else { + unicode.append(HexUtil.toUnicodeHex(c)); + } + } + return unicode.toString(); + } +} diff --git a/src/main/java/cn/hutool/core/text/csv/CsvBaseReader.java b/src/main/java/cn/hutool/core/text/csv/CsvBaseReader.java new file mode 100644 index 0000000..2ac2f91 --- /dev/null +++ b/src/main/java/cn/hutool/core/text/csv/CsvBaseReader.java @@ -0,0 +1,280 @@ +package cn.hutool.core.text.csv; + +import cn.hutool.core.io.FileUtil; +import cn.hutool.core.io.IORuntimeException; +import cn.hutool.core.io.IoUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.CharsetUtil; +import cn.hutool.core.util.ObjectUtil; + +import java.io.File; +import java.io.Reader; +import java.io.Serializable; +import java.io.StringReader; +import java.nio.charset.Charset; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +/** + * CSV文件读取器基础类,提供灵活的文件、路径中的CSV读取,一次构造可多次调用读取不同数据,参考:FastCSV + * + * @author Looly + * @since 5.0.4 + */ +public class CsvBaseReader implements Serializable { + private static final long serialVersionUID = 1L; + + /** + * 默认编码 + */ + protected static final Charset DEFAULT_CHARSET = CharsetUtil.CHARSET_UTF_8; + + private final CsvReadConfig config; + + //--------------------------------------------------------------------------------------------- Constructor start + + /** + * 构造,使用默认配置项 + */ + public CsvBaseReader() { + this(null); + } + + /** + * 构造 + * + * @param config 配置项 + */ + public CsvBaseReader(CsvReadConfig config) { + this.config = ObjectUtil.defaultIfNull(config, CsvReadConfig::defaultConfig); + } + //--------------------------------------------------------------------------------------------- Constructor end + + /** + * 设置字段分隔符,默认逗号',' + * + * @param fieldSeparator 字段分隔符,默认逗号',' + */ + public void setFieldSeparator(char fieldSeparator) { + this.config.setFieldSeparator(fieldSeparator); + } + + /** + * 设置 文本分隔符,文本包装符,默认双引号'"' + * + * @param textDelimiter 文本分隔符,文本包装符,默认双引号'"' + */ + public void setTextDelimiter(char textDelimiter) { + this.config.setTextDelimiter(textDelimiter); + } + + /** + * 设置是否首行做为标题行,默认false + * + * @param containsHeader 是否首行做为标题行,默认false + */ + public void setContainsHeader(boolean containsHeader) { + this.config.setContainsHeader(containsHeader); + } + + /** + * 设置是否跳过空白行,默认true + * + * @param skipEmptyRows 是否跳过空白行,默认true + */ + public void setSkipEmptyRows(boolean skipEmptyRows) { + this.config.setSkipEmptyRows(skipEmptyRows); + } + + /** + * 设置每行字段个数不同时是否抛出异常,默认false + * + * @param errorOnDifferentFieldCount 每行字段个数不同时是否抛出异常,默认false + */ + public void setErrorOnDifferentFieldCount(boolean errorOnDifferentFieldCount) { + this.config.setErrorOnDifferentFieldCount(errorOnDifferentFieldCount); + } + + /** + * 读取CSV文件,默认UTF-8编码 + * + * @param file CSV文件 + * @return {@link CsvData},包含数据列表和行信息 + * @throws IORuntimeException IO异常 + */ + public CsvData read(File file) throws IORuntimeException { + return read(file, DEFAULT_CHARSET); + } + + /** + * 从字符串中读取CSV数据 + * + * @param csvStr CSV字符串 + * @return {@link CsvData},包含数据列表和行信息 + */ + public CsvData readFromStr(String csvStr) { + return read(new StringReader(csvStr)); + } + + /** + * 从字符串中读取CSV数据 + * + * @param csvStr CSV字符串 + * @param rowHandler 行处理器,用于一行一行的处理数据 + */ + public void readFromStr(String csvStr, CsvRowHandler rowHandler) { + read(parse(new StringReader(csvStr)), rowHandler); + } + + + /** + * 读取CSV文件 + * + * @param file CSV文件 + * @param charset 文件编码,默认系统编码 + * @return {@link CsvData},包含数据列表和行信息 + * @throws IORuntimeException IO异常 + */ + public CsvData read(File file, Charset charset) throws IORuntimeException { + return read(Objects.requireNonNull(file.toPath(), "file must not be null"), charset); + } + + /** + * 读取CSV文件,默认UTF-8编码 + * + * @param path CSV文件 + * @return {@link CsvData},包含数据列表和行信息 + * @throws IORuntimeException IO异常 + */ + public CsvData read(Path path) throws IORuntimeException { + return read(path, DEFAULT_CHARSET); + } + + /** + * 读取CSV文件 + * + * @param path CSV文件 + * @param charset 文件编码,默认系统编码 + * @return {@link CsvData},包含数据列表和行信息 + * @throws IORuntimeException IO异常 + */ + public CsvData read(Path path, Charset charset) throws IORuntimeException { + Assert.notNull(path, "path must not be null"); + return read(FileUtil.getReader(path, charset)); + } + + /** + * 从Reader中读取CSV数据,读取后关闭Reader + * + * @param reader Reader + * @return {@link CsvData},包含数据列表和行信息 + * @throws IORuntimeException IO异常 + */ + public CsvData read(Reader reader) throws IORuntimeException { + final CsvParser csvParser = parse(reader); + final List rows = new ArrayList<>(); + read(csvParser, rows::add); + final List header = config.headerLineNo > -1 ? csvParser.getHeader() : null; + + return new CsvData(header, rows); + } + + /** + * 从Reader中读取CSV数据,结果为Map,读取后关闭Reader。
+ * 此方法默认识别首行为标题行。 + * + * @param reader Reader + * @return {@link CsvData},包含数据列表和行信息 + * @throws IORuntimeException IO异常 + */ + public List> readMapList(Reader reader) throws IORuntimeException { + // 此方法必须包含标题 + this.config.setContainsHeader(true); + + final List> result = new ArrayList<>(); + read(reader, (row) -> result.add(row.getFieldMap())); + return result; + } + + /** + * 从Reader中读取CSV数据并转换为Bean列表,读取后关闭Reader。
+ * 此方法默认识别首行为标题行。 + * + * @param Bean类型 + * @param reader Reader + * @param clazz Bean类型 + * @return Bean列表 + */ + public List read(Reader reader, Class clazz) { + // 此方法必须包含标题 + this.config.setContainsHeader(true); + + final List result = new ArrayList<>(); + read(reader, (row) -> result.add(row.toBean(clazz))); + return result; + } + + /** + * 从字符串中读取CSV数据并转换为Bean列表,读取后关闭Reader。
+ * 此方法默认识别首行为标题行。 + * + * @param Bean类型 + * @param csvStr csv字符串 + * @param clazz Bean类型 + * @return Bean列表 + */ + public List read(String csvStr, Class clazz) { + // 此方法必须包含标题 + this.config.setContainsHeader(true); + + final List result = new ArrayList<>(); + read(new StringReader(csvStr), (row) -> result.add(row.toBean(clazz))); + return result; + } + + /** + * 从Reader中读取CSV数据,读取后关闭Reader + * + * @param reader Reader + * @param rowHandler 行处理器,用于一行一行的处理数据 + * @throws IORuntimeException IO异常 + */ + public void read(Reader reader, CsvRowHandler rowHandler) throws IORuntimeException { + read(parse(reader), rowHandler); + } + + //--------------------------------------------------------------------------------------------- Private method start + + /** + * 读取CSV数据,读取后关闭Parser + * + * @param csvParser CSV解析器 + * @param rowHandler 行处理器,用于一行一行的处理数据 + * @throws IORuntimeException IO异常 + * @since 5.0.4 + */ + private void read(CsvParser csvParser, CsvRowHandler rowHandler) throws IORuntimeException { + try { + while (csvParser.hasNext()){ + rowHandler.handle(csvParser.next()); + } + } finally { + IoUtil.close(csvParser); + } + } + + /** + * 构建 {@link CsvParser} + * + * @param reader Reader + * @return CsvParser + * @throws IORuntimeException IO异常 + */ + protected CsvParser parse(Reader reader) throws IORuntimeException { + return new CsvParser(reader, this.config); + } + //--------------------------------------------------------------------------------------------- Private method start +} diff --git a/src/main/java/cn/hutool/core/text/csv/CsvConfig.java b/src/main/java/cn/hutool/core/text/csv/CsvConfig.java new file mode 100644 index 0000000..7798995 --- /dev/null +++ b/src/main/java/cn/hutool/core/text/csv/CsvConfig.java @@ -0,0 +1,119 @@ +package cn.hutool.core.text.csv; + +import cn.hutool.core.util.CharUtil; + +import java.io.Serializable; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * CSV基础配置项,此配置项可用于读取和写出CSV,定义了包括字段分隔符、文本包装符等符号 + * + * @param 继承子类类型,用于this返回 + * @author looly + * @since 4.0.5 + */ +@SuppressWarnings("unchecked") +public class CsvConfig> implements Serializable { + private static final long serialVersionUID = -8069578249066158459L; + + /** + * 字段分隔符,默认逗号',' + */ + protected char fieldSeparator = CharUtil.COMMA; + /** + * 文本包装符,默认双引号'"' + */ + protected char textDelimiter = CharUtil.DOUBLE_QUOTES; + /** + * 注释符号,用于区分注释行,默认'#' + */ + protected Character commentCharacter = '#'; + /** + * 标题别名 + */ + protected Map headerAlias = new LinkedHashMap<>(); + + /** + * 设置字段分隔符,默认逗号',' + * + * @param fieldSeparator 字段分隔符,默认逗号',' + * @return this + */ + public T setFieldSeparator(final char fieldSeparator) { + this.fieldSeparator = fieldSeparator; + return (T) this; + } + + /** + * 设置 文本分隔符,文本包装符,默认双引号'"' + * + * @param textDelimiter 文本分隔符,文本包装符,默认双引号'"' + * @return this + */ + public T setTextDelimiter(char textDelimiter) { + this.textDelimiter = textDelimiter; + return (T) this; + } + + /** + * 设置注释无效
+ * 当写出CSV时,{@link CsvWriter#writeComment(String)}将抛出异常
+ * 当读取CSV时,注释行按照正常行读取 + * + * @return this + * @since 5.7.14 + */ + public T disableComment() { + return setCommentCharacter(null); + } + + /** + * 设置 注释符号,用于区分注释行,{@code null}表示忽略注释 + * + * @param commentCharacter 注释符号,用于区分注释行 + * @return this + * @since 5.5.7 + */ + public T setCommentCharacter(Character commentCharacter) { + this.commentCharacter = commentCharacter; + return (T) this; + } + + /** + * 设置标题行的别名Map + * + * @param headerAlias 别名Map + * @return this + * @since 5.7.10 + */ + public T setHeaderAlias(Map headerAlias) { + this.headerAlias = headerAlias; + return (T) this; + } + + /** + * 增加标题别名 + * + * @param header 标题 + * @param alias 别名 + * @return this + * @since 5.7.10 + */ + public T addHeaderAlias(String header, String alias) { + this.headerAlias.put(header, alias); + return (T) this; + } + + /** + * 去除标题别名 + * + * @param header 标题 + * @return this + * @since 5.7.10 + */ + public T removeHeaderAlias(String header) { + this.headerAlias.remove(header); + return (T) this; + } +} diff --git a/src/main/java/cn/hutool/core/text/csv/CsvData.java b/src/main/java/cn/hutool/core/text/csv/CsvData.java new file mode 100644 index 0000000..bd852ad --- /dev/null +++ b/src/main/java/cn/hutool/core/text/csv/CsvData.java @@ -0,0 +1,83 @@ +package cn.hutool.core.text.csv; + +import java.io.Serializable; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; + +/** + * CSV数据,包括头部信息和行数据,参考:FastCSV + * + * @author Looly + */ +public class CsvData implements Iterable, Serializable { + private static final long serialVersionUID = 1L; + + private final List header; + private final List rows; + + /** + * 构造 + * + * @param header 头信息, 可以为null + * @param rows 行 + */ + public CsvData(final List header, final List rows) { + this.header = header; + this.rows = rows; + } + + /** + * 总行数 + * + * @return 总行数 + */ + public int getRowCount() { + return this.rows.size(); + } + + /** + * 获取头信息列表,如果无头信息为{@code Null},返回列表为只读列表 + * + * @return the header row - might be {@code null} if no header exists + */ + public List getHeader() { + if(null == this.header){ + return null; + } + return Collections.unmodifiableList(this.header); + } + + /** + * 获取指定行,从0开始 + * + * @param index 行号 + * @return 行数据 + * @throws IndexOutOfBoundsException if index is out of range + */ + public CsvRow getRow(final int index) { + return this.rows.get(index); + } + + /** + * 获取所有行 + * + * @return 所有行 + */ + public List getRows() { + return this.rows; + } + + @Override + public Iterator iterator() { + return this.rows.iterator(); + } + + @Override + public String toString() { + return "CsvData{" + + "header=" + header + + ", rows=" + rows + + '}'; + } +} diff --git a/src/main/java/cn/hutool/core/text/csv/CsvParser.java b/src/main/java/cn/hutool/core/text/csv/CsvParser.java new file mode 100644 index 0000000..eb9ef4d --- /dev/null +++ b/src/main/java/cn/hutool/core/text/csv/CsvParser.java @@ -0,0 +1,447 @@ +package cn.hutool.core.text.csv; + +import cn.hutool.core.collection.ComputeIter; +import cn.hutool.core.io.IORuntimeException; +import cn.hutool.core.io.IoUtil; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.text.StrBuilder; +import cn.hutool.core.util.CharUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; + +import java.io.Closeable; +import java.io.IOException; +import java.io.Reader; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +/** + * CSV行解析器,参考:FastCSV + * + * @author Looly + */ +public final class CsvParser extends ComputeIter implements Closeable, Serializable { + private static final long serialVersionUID = 1L; + + private static final int DEFAULT_ROW_CAPACITY = 10; + + private final Reader reader; + private final CsvReadConfig config; + + private final Buffer buf = new Buffer(IoUtil.DEFAULT_LARGE_BUFFER_SIZE); + /** + * 前一个特殊分界字符 + */ + private int preChar = -1; + /** + * 是否在引号包装内 + */ + private boolean inQuotes; + /** + * 当前读取字段 + */ + private final StrBuilder currentField = new StrBuilder(512); + + /** + * 标题行 + */ + private CsvRow header; + /** + * 当前行号 + */ + private long lineNo = -1; + /** + * 引号内的行数 + */ + private long inQuotesLineCount; + /** + * 第一行字段数,用于检查每行字段数是否一致 + */ + private int firstLineFieldCount = -1; + /** + * 最大字段数量,用于初始化行,减少扩容 + */ + private int maxFieldCount; + /** + * 是否读取结束 + */ + private boolean finished; + + /** + * CSV解析器 + * + * @param reader Reader + * @param config 配置,null则为默认配置 + */ + public CsvParser(final Reader reader, CsvReadConfig config) { + this.reader = Objects.requireNonNull(reader, "reader must not be null"); + this.config = ObjectUtil.defaultIfNull(config, CsvReadConfig::defaultConfig); + } + + /** + * 获取头部字段列表,如果headerLineNo < 0,抛出异常 + * + * @return 头部列表 + * @throws IllegalStateException 如果不解析头部或者没有调用nextRow()方法 + */ + public List getHeader() { + if (config.headerLineNo < 0) { + throw new IllegalStateException("No header available - header parsing is disabled"); + } + if (lineNo < config.beginLineNo) { + throw new IllegalStateException("No header available - call nextRow() first"); + } + return header.fields; + } + + @Override + protected CsvRow computeNext() { + return nextRow(); + } + + /** + * 读取下一行数据 + * + * @return CsvRow + * @throws IORuntimeException IO读取异常 + */ + public CsvRow nextRow() throws IORuntimeException { + List currentFields; + int fieldCount; + while (!finished) { + currentFields = readLine(); + fieldCount = currentFields.size(); + if (fieldCount < 1) { + // 空List表示读取结束 + break; + } + + // 读取范围校验 + if(lineNo < config.beginLineNo){ + // 未达到读取起始行,继续 + continue; + } + if(lineNo > config.endLineNo){ + // 超出结束行,读取结束 + break; + } + + // 跳过空行 + if (config.skipEmptyRows && fieldCount == 1 && currentFields.get(0).isEmpty()) { + // [""]表示空行 + continue; + } + + // 检查每行的字段数是否一致 + if (config.errorOnDifferentFieldCount) { + if (firstLineFieldCount < 0) { + firstLineFieldCount = fieldCount; + } else if (fieldCount != firstLineFieldCount) { + throw new IORuntimeException(String.format("Line %d has %d fields, but first line has %d fields", lineNo, fieldCount, firstLineFieldCount)); + } + } + + // 记录最大字段数 + if (fieldCount > maxFieldCount) { + maxFieldCount = fieldCount; + } + + //初始化标题 + if (lineNo == config.headerLineNo && null == header) { + initHeader(currentFields); + // 作为标题行后,此行跳过,下一行做为第一行 + continue; + } + + return new CsvRow(lineNo, null == header ? null : header.headerMap, currentFields); + } + + return null; + } + + /** + * 当前行做为标题行 + * + * @param currentFields 当前行字段列表 + */ + private void initHeader(final List currentFields) { + final Map localHeaderMap = new LinkedHashMap<>(currentFields.size()); + for (int i = 0; i < currentFields.size(); i++) { + String field = currentFields.get(i); + if (MapUtil.isNotEmpty(this.config.headerAlias)) { + // 自定义别名 + field = ObjectUtil.defaultIfNull(this.config.headerAlias.get(field), field); + } + if (StrUtil.isNotEmpty(field) && !localHeaderMap.containsKey(field)) { + localHeaderMap.put(field, i); + } + } + + header = new CsvRow(this.lineNo, Collections.unmodifiableMap(localHeaderMap), Collections.unmodifiableList(currentFields)); + } + + /** + * 读取一行数据,如果读取结束,返回size为0的List
+ * 空行是size为1的List,唯一元素是"" + * + *

+ * 行号要考虑注释行和引号包装的内容中的换行 + *

+ * + * @return 一行数据 + * @throws IORuntimeException IO异常 + */ + private List readLine() throws IORuntimeException { + // 矫正行号 + // 当一行内容包含多行数据时,记录首行行号,但是读取下一行时,需要把多行内容的行数加上 + if(inQuotesLineCount > 0){ + this.lineNo += this.inQuotesLineCount; + this.inQuotesLineCount = 0; + } + + final List currentFields = new ArrayList<>(maxFieldCount > 0 ? maxFieldCount : DEFAULT_ROW_CAPACITY); + + final StrBuilder currentField = this.currentField; + final Buffer buf = this.buf; + int preChar = this.preChar;//前一个特殊分界字符 + int copyLen = 0; //拷贝长度 + boolean inComment = false; + + while (true) { + if (!buf.hasRemaining()) { + // 此Buffer读取结束,开始读取下一段 + if (copyLen > 0) { + buf.appendTo(currentField, copyLen); + // 此处无需mark,read方法会重置mark + } + if (buf.read(this.reader) < 0) { + // CSV读取结束 + finished = true; + + if (currentField.hasContent() || preChar == config.fieldSeparator) { + //剩余部分作为一个字段 + addField(currentFields, currentField.toStringAndReset()); + } + break; + } + + //重置 + copyLen = 0; + } + + final char c = buf.get(); + + // 注释行标记 + if(preChar < 0 || preChar == CharUtil.CR || preChar == CharUtil.LF){ + // 判断行首字符为指定注释字符的注释开始,直到遇到换行符 + // 行首分两种,1是preChar < 0表示文本开始,2是换行符后紧跟就是下一行的开始 + if(null != this.config.commentCharacter && c == this.config.commentCharacter){ + inComment = true; + } + } + // 注释行处理 + if(inComment){ + if (c == CharUtil.CR || c == CharUtil.LF) { + // 注释行以换行符为结尾 + lineNo++; + inComment = false; + } + // 跳过注释行中的任何字符 + buf.mark(); + preChar = c; + continue; + } + + if (inQuotes) { + //引号内,作为内容,直到引号结束 + if (c == config.textDelimiter) { + // End of quoted text + inQuotes = false; + } else { + // 字段内容中新行 + if (isLineEnd(c, preChar)) { + inQuotesLineCount++; + } + } + // 普通字段字符 + copyLen++; + } else { + // 非引号内 + if (c == config.fieldSeparator) { + //一个字段结束 + if (copyLen > 0) { + buf.appendTo(currentField, copyLen); + copyLen = 0; + } + buf.mark(); + addField(currentFields, currentField.toStringAndReset()); + } else if (c == config.textDelimiter) { + // 引号开始 + inQuotes = true; + copyLen++; + } else if (c == CharUtil.CR) { + // \r,直接结束 + if (copyLen > 0) { + buf.appendTo(currentField, copyLen); + } + buf.mark(); + addField(currentFields, currentField.toStringAndReset()); + preChar = c; + break; + } else if (c == CharUtil.LF) { + // \n + if (preChar != CharUtil.CR) { + if (copyLen > 0) { + buf.appendTo(currentField, copyLen); + } + buf.mark(); + addField(currentFields, currentField.toStringAndReset()); + preChar = c; + break; + } + // 前一个字符是\r,已经处理过这个字段了,此处直接跳过 + buf.mark(); + } else { + // 普通字符 + copyLen++; + } + } + + preChar = c; + } + + // restore fields + this.preChar = preChar; + + lineNo++; + return currentFields; + } + + @Override + public void close() throws IOException { + reader.close(); + } + + /** + * 将字段加入字段列表并自动去包装和去转义 + * + * @param currentFields 当前的字段列表(即为行) + * @param field 字段 + */ + private void addField(List currentFields, String field) { + final char textDelimiter = this.config.textDelimiter; + + // 忽略多余引号后的换行符 + field = StrUtil.trim(field, 1, (c-> c == CharUtil.LF || c == CharUtil.CR)); + + field = StrUtil.unWrap(field, textDelimiter); + field = StrUtil.replace(field, String.valueOf(textDelimiter) + textDelimiter, String.valueOf(textDelimiter)); + if(this.config.trimField){ + // issue#I49M0C@Gitee + field = StrUtil.trim(field); + } + currentFields.add(field); + } + + /** + * 是否行结束符 + * + * @param c 符号 + * @param preChar 前一个字符 + * @return 是否结束 + * @since 5.7.4 + */ + private boolean isLineEnd(char c, int preChar) { + return (c == CharUtil.CR || c == CharUtil.LF) && preChar != CharUtil.CR; + } + + /** + * 内部Buffer + * + * @author looly + */ + private static class Buffer implements Serializable{ + private static final long serialVersionUID = 1L; + + final char[] buf; + + /** + * 标记位置,用于读数据 + */ + private int mark; + /** + * 当前位置 + */ + private int position; + /** + * 读取的数据长度,一般小于buf.length,-1表示无数据 + */ + private int limit; + + Buffer(int capacity) { + buf = new char[capacity]; + } + + /** + * 是否还有未读数据 + * + * @return 是否还有未读数据 + */ + public final boolean hasRemaining() { + return position < limit; + } + + /** + * 读取到缓存
+ * 全量读取,会重置Buffer中所有数据 + * + * @param reader {@link Reader} + */ + int read(Reader reader) { + int length; + try { + length = reader.read(this.buf); + } catch (IOException e) { + throw new IORuntimeException(e); + } + this.mark = 0; + this.position = 0; + this.limit = length; + return length; + } + + /** + * 先获取当前字符,再将当前位置后移一位
+ * 此方法不检查是否到了数组末尾,请自行使用{@link #hasRemaining()}判断。 + * + * @return 当前位置字符 + * @see #hasRemaining() + */ + char get() { + return this.buf[this.position++]; + } + + /** + * 标记位置记为下次读取位置 + */ + void mark() { + this.mark = this.position; + } + + /** + * 将数据追加到{@link StrBuilder},追加结束后需手动调用{@link #mark()} 重置读取位置 + * + * @param builder {@link StrBuilder} + * @param length 追加的长度 + * @see #mark() + */ + void appendTo(StrBuilder builder, int length) { + builder.append(this.buf, this.mark, length); + } + } +} diff --git a/src/main/java/cn/hutool/core/text/csv/CsvReadConfig.java b/src/main/java/cn/hutool/core/text/csv/CsvReadConfig.java new file mode 100644 index 0000000..5ad7c7d --- /dev/null +++ b/src/main/java/cn/hutool/core/text/csv/CsvReadConfig.java @@ -0,0 +1,118 @@ +package cn.hutool.core.text.csv; + +import java.io.Serializable; + +/** + * CSV读取配置项 + * + * @author looly + * + */ +public class CsvReadConfig extends CsvConfig implements Serializable { + private static final long serialVersionUID = 5396453565371560052L; + + /** 指定标题行号,-1表示无标题行 */ + protected long headerLineNo = -1; + /** 是否跳过空白行,默认true */ + protected boolean skipEmptyRows = true; + /** 每行字段个数不同时是否抛出异常,默认false */ + protected boolean errorOnDifferentFieldCount; + /** 定义开始的行(包括),此处为原始文件行号 */ + protected long beginLineNo; + /** 结束的行(包括),此处为原始文件行号 */ + protected long endLineNo = Long.MAX_VALUE-1; + /** 每个字段是否去除两边空白符 */ + protected boolean trimField; + + /** + * 默认配置 + * + * @return 默认配置 + */ + public static CsvReadConfig defaultConfig() { + return new CsvReadConfig(); + } + + /** + * 设置是否首行做为标题行,默认false
+ * 当设置为{@code true}时,默认标题行号是{@link #beginLineNo},{@code false}为-1,表示无行号 + * + * @param containsHeader 是否首行做为标题行,默认false + * @return this + * @see #setHeaderLineNo(long) + */ + public CsvReadConfig setContainsHeader(boolean containsHeader) { + return setHeaderLineNo(containsHeader ? beginLineNo : -1); + } + + /** + * 设置标题行行号,默认-1,表示无标题行
+ * + * @param headerLineNo 标题行行号,-1表示无标题行 + * @return this + * @since 5.7.23 + */ + public CsvReadConfig setHeaderLineNo(long headerLineNo) { + this.headerLineNo = headerLineNo; + return this; + } + + /** + * 设置是否跳过空白行,默认true + * + * @param skipEmptyRows 是否跳过空白行,默认true + * @return this + */ + public CsvReadConfig setSkipEmptyRows(boolean skipEmptyRows) { + this.skipEmptyRows = skipEmptyRows; + return this; + } + + /** + * 设置每行字段个数不同时是否抛出异常,默认false + * + * @param errorOnDifferentFieldCount 每行字段个数不同时是否抛出异常,默认false + * @return this + */ + public CsvReadConfig setErrorOnDifferentFieldCount(boolean errorOnDifferentFieldCount) { + this.errorOnDifferentFieldCount = errorOnDifferentFieldCount; + return this; + } + + /** + * 设置开始的行(包括),默认0,此处为原始文件行号 + * + * @param beginLineNo 开始的行号(包括) + * @return this + * @since 5.7.4 + */ + public CsvReadConfig setBeginLineNo(long beginLineNo) { + this.beginLineNo = beginLineNo; + return this; + } + + /** + * 设置结束的行(包括),默认不限制,此处为原始文件行号 + * + * @param endLineNo 结束的行号(包括) + * @return this + * @since 5.7.4 + */ + public CsvReadConfig setEndLineNo(long endLineNo) { + this.endLineNo = endLineNo; + return this; + } + + /** + * 设置每个字段是否去除两边空白符
+ * 如果字段以{@link #textDelimiter}包围,则保留两边空格 + * + * @param trimField 去除两边空白符 + * @return this + * @since 5.7.13 + */ + public CsvReadConfig setTrimField(boolean trimField) { + this.trimField = trimField; + return this; + } +} diff --git a/src/main/java/cn/hutool/core/text/csv/CsvReader.java b/src/main/java/cn/hutool/core/text/csv/CsvReader.java new file mode 100644 index 0000000..15065cb --- /dev/null +++ b/src/main/java/cn/hutool/core/text/csv/CsvReader.java @@ -0,0 +1,153 @@ +package cn.hutool.core.text.csv; + +import cn.hutool.core.io.FileUtil; +import cn.hutool.core.io.IORuntimeException; +import cn.hutool.core.io.IoUtil; + +import java.io.Closeable; +import java.io.File; +import java.io.IOException; +import java.io.Reader; +import java.nio.charset.Charset; +import java.nio.file.Path; +import java.util.Iterator; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; + +/** + * CSV文件读取器,参考:FastCSV + * + * @author Looly + * @since 4.0.1 + */ +public class CsvReader extends CsvBaseReader implements Iterable, Closeable { + private static final long serialVersionUID = 1L; + + private final Reader reader; + + //--------------------------------------------------------------------------------------------- Constructor start + + /** + * 构造,使用默认配置项 + */ + public CsvReader() { + this(null); + } + + /** + * 构造 + * + * @param config 配置项 + */ + public CsvReader(CsvReadConfig config) { + this((Reader) null, config); + } + + /** + * 构造,默认{@link #DEFAULT_CHARSET}编码 + * + * @param file CSV文件路径,null表示不设置路径 + * @param config 配置项,null表示默认配置 + * @since 5.0.4 + */ + public CsvReader(File file, CsvReadConfig config) { + this(file, DEFAULT_CHARSET, config); + } + + /** + * 构造,默认{@link #DEFAULT_CHARSET}编码 + * + * @param path CSV文件路径,null表示不设置路径 + * @param config 配置项,null表示默认配置 + * @since 5.0.4 + */ + public CsvReader(Path path, CsvReadConfig config) { + this(path, DEFAULT_CHARSET, config); + } + + /** + * 构造 + * + * @param file CSV文件路径,null表示不设置路径 + * @param charset 编码 + * @param config 配置项,null表示默认配置 + * @since 5.0.4 + */ + public CsvReader(File file, Charset charset, CsvReadConfig config) { + this(FileUtil.getReader(file, charset), config); + } + + /** + * 构造 + * + * @param path CSV文件路径,null表示不设置路径 + * @param charset 编码 + * @param config 配置项,null表示默认配置 + * @since 5.0.4 + */ + public CsvReader(Path path, Charset charset, CsvReadConfig config) { + this(FileUtil.getReader(path, charset), config); + } + + /** + * 构造 + * + * @param reader {@link Reader},null表示不设置默认reader + * @param config 配置项,null表示默认配置 + * @since 5.0.4 + */ + public CsvReader(Reader reader, CsvReadConfig config) { + super(config); + this.reader = reader; + } + //--------------------------------------------------------------------------------------------- Constructor end + /** + * 读取CSV文件,此方法只能调用一次
+ * 调用此方法的前提是构造中传入文件路径或Reader + * + * @return {@link CsvData},包含数据列表和行信息 + * @throws IORuntimeException IO异常 + */ + public CsvData read() throws IORuntimeException { + return read(this.reader); + } + + /** + * 读取CSV数据,此方法只能调用一次
+ * 调用此方法的前提是构造中传入文件路径或Reader + * + * @param rowHandler 行处理器,用于一行一行的处理数据 + * @throws IORuntimeException IO异常 + * @since 5.0.4 + */ + public void read(CsvRowHandler rowHandler) throws IORuntimeException { + read(this.reader, rowHandler); + } + + /** + * 根据Reader创建{@link Stream},以便使用stream方式读取csv行 + * + * @return {@link Stream} + * @since 5.7.14 + */ + public Stream stream() { + return StreamSupport.stream(spliterator(), false) + .onClose(() -> { + try { + close(); + } catch (final IOException e) { + throw new IORuntimeException(e); + } + }); + } + + @Override + public Iterator iterator() { + return parse(this.reader); + } + + @Override + public void close() throws IOException { + IoUtil.close(this.reader); + } +} diff --git a/src/main/java/cn/hutool/core/text/csv/CsvRow.java b/src/main/java/cn/hutool/core/text/csv/CsvRow.java new file mode 100644 index 0000000..fcc37f5 --- /dev/null +++ b/src/main/java/cn/hutool/core/text/csv/CsvRow.java @@ -0,0 +1,267 @@ +package cn.hutool.core.text.csv; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.lang.Assert; + +import java.util.Collection; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.ListIterator; +import java.util.Map; + +/** + * CSV中一行的表示 + * + * @author Looly + */ +public final class CsvRow implements List { + + /** 原始行号 */ + private final long originalLineNumber; + + final Map headerMap; + final List fields; + + /** + * 构造 + * + * @param originalLineNumber 对应文件中的第几行 + * @param headerMap 标题Map + * @param fields 数据列表 + */ + public CsvRow(long originalLineNumber, Map headerMap, List fields) { + Assert.notNull(fields, "fields must be not null!"); + this.originalLineNumber = originalLineNumber; + this.headerMap = headerMap; + this.fields = fields; + } + + /** + * 获取原始行号,多行情况下为首行行号。忽略注释行 + * + * @return the original line number 行号 + */ + public long getOriginalLineNumber() { + return originalLineNumber; + } + + /** + * 获取标题对应的字段内容 + * + * @param name 标题名 + * @return 字段值,null表示无此字段值 + * @throws IllegalStateException CSV文件无标题行抛出此异常 + */ + public String getByName(String name) { + Assert.notNull(this.headerMap, "No header available!"); + + final Integer col = headerMap.get(name); + if (col != null) { + return get(col); + } + return null; + } + + /** + * 获取本行所有字段值列表 + * + * @return 字段值列表 + */ + public List getRawList() { + return fields; + } + + /** + * 获取标题与字段值对应的Map + * + * @return 标题与字段值对应的Map + * @throws IllegalStateException CSV文件无标题行抛出此异常 + */ + public Map getFieldMap() { + if (headerMap == null) { + throw new IllegalStateException("No header available"); + } + + final Map fieldMap = new LinkedHashMap<>(headerMap.size(), 1); + String key; + Integer col; + String val; + for (final Map.Entry header : headerMap.entrySet()) { + key = header.getKey(); + col = headerMap.get(key); + val = null == col ? null : get(col); + fieldMap.put(key, val); + } + + return fieldMap; + } + + /** + * 一行数据转换为Bean对象 + * + * @param Bean类型 + * @param clazz bean类 + * @return Bean + * @since 5.3.6 + */ + public T toBean(Class clazz){ + return BeanUtil.toBeanIgnoreError(getFieldMap(), clazz); + } + + /** + * 获取字段格式 + * + * @return 字段格式 + */ + public int getFieldCount() { + return fields.size(); + } + + @Override + public int size() { + return this.fields.size(); + } + + @Override + public boolean isEmpty() { + return this.fields.isEmpty(); + } + + @Override + public boolean contains(Object o) { + return this.fields.contains(o); + } + + @Override + public Iterator iterator() { + return this.fields.iterator(); + } + + @Override + public Object[] toArray() { + return this.fields.toArray(); + } + + @Override + public T[] toArray(T[] a) { + //noinspection SuspiciousToArrayCall + return this.fields.toArray(a); + } + + @Override + public boolean add(String e) { + return this.fields.add(e); + } + + @Override + public boolean remove(Object o) { + return this.fields.remove(o); + } + + @Override + public boolean containsAll(Collection c) { + return this.fields.containsAll(c); + } + + @Override + public boolean addAll(Collection c) { + return this.fields.addAll(c); + } + + @Override + public boolean addAll(int index, Collection c) { + return this.fields.addAll(index, c); + } + + @Override + public boolean removeAll(Collection c) { + return this.fields.removeAll(c); + } + + @Override + public boolean retainAll(Collection c) { + return this.fields.retainAll(c); + } + + @Override + public void clear() { + this.fields.clear(); + } + + @Override + public String get(int index) { + return index >= fields.size() ? null : fields.get(index); + } + + @Override + public String set(int index, String element) { + return this.fields.set(index, element); + } + + @Override + public void add(int index, String element) { + this.fields.add(index, element); + } + + @Override + public String remove(int index) { + return this.fields.remove(index); + } + + @Override + public int indexOf(Object o) { + return this.fields.indexOf(o); + } + + @Override + public int lastIndexOf(Object o) { + return this.fields.lastIndexOf(o); + } + + @Override + public ListIterator listIterator() { + return this.fields.listIterator(); + } + + @Override + public ListIterator listIterator(int index) { + return this.fields.listIterator(index); + } + + @Override + public List subList(int fromIndex, int toIndex) { + return this.fields.subList(fromIndex, toIndex); + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder("CsvRow{"); + sb.append("originalLineNumber="); + sb.append(originalLineNumber); + sb.append(", "); + + sb.append("fields="); + if (headerMap != null) { + sb.append('{'); + for (final Iterator> it = getFieldMap().entrySet().iterator(); it.hasNext();) { + + final Map.Entry entry = it.next(); + sb.append(entry.getKey()); + sb.append('='); + if (entry.getValue() != null) { + sb.append(entry.getValue()); + } + if (it.hasNext()) { + sb.append(", "); + } + } + sb.append('}'); + } else { + sb.append(fields.toString()); + } + + sb.append('}'); + return sb.toString(); + } +} diff --git a/src/main/java/cn/hutool/core/text/csv/CsvRowHandler.java b/src/main/java/cn/hutool/core/text/csv/CsvRowHandler.java new file mode 100644 index 0000000..c5a5014 --- /dev/null +++ b/src/main/java/cn/hutool/core/text/csv/CsvRowHandler.java @@ -0,0 +1,18 @@ +package cn.hutool.core.text.csv; + +/** + * CSV的行处理器,实现此接口用于按照行处理数据 + * + * @author Looly + * @since 5.0.4 + */ +@FunctionalInterface +public interface CsvRowHandler { + + /** + * 处理行数据 + * + * @param row 行数据 + */ + void handle(CsvRow row); +} diff --git a/src/main/java/cn/hutool/core/text/csv/CsvUtil.java b/src/main/java/cn/hutool/core/text/csv/CsvUtil.java new file mode 100644 index 0000000..ea18400 --- /dev/null +++ b/src/main/java/cn/hutool/core/text/csv/CsvUtil.java @@ -0,0 +1,141 @@ +package cn.hutool.core.text.csv; + +import java.io.File; +import java.io.Reader; +import java.io.Writer; +import java.nio.charset.Charset; + +/** + * CSV工具 + * + * @author looly + * @since 4.0.5 + */ +public class CsvUtil { + + //----------------------------------------------------------------------------------------------------------- Reader + + /** + * 获取CSV读取器,调用此方法创建的Reader须自行指定读取的资源 + * + * @param config 配置, 允许为空. + * @return {@link CsvReader} + */ + public static CsvReader getReader(CsvReadConfig config) { + return new CsvReader(config); + } + + /** + * 获取CSV读取器,调用此方法创建的Reader须自行指定读取的资源 + * + * @return {@link CsvReader} + */ + public static CsvReader getReader() { + return new CsvReader(); + } + + /** + * 获取CSV读取器 + * + * @param reader {@link Reader} + * @param config 配置, {@code null}表示默认配置 + * @return {@link CsvReader} + * @since 5.7.14 + */ + public static CsvReader getReader(Reader reader, CsvReadConfig config) { + return new CsvReader(reader, config); + } + + /** + * 获取CSV读取器 + * + * @param reader {@link Reader} + * @return {@link CsvReader} + * @since 5.7.14 + */ + public static CsvReader getReader(Reader reader) { + return getReader(reader, null); + } + + //----------------------------------------------------------------------------------------------------------- Writer + + /** + * 获取CSV生成器(写出器),使用默认配置,覆盖已有文件(如果存在) + * + * @param filePath File CSV文件路径 + * @param charset 编码 + * @return {@link CsvWriter} + */ + public static CsvWriter getWriter(String filePath, Charset charset) { + return new CsvWriter(filePath, charset); + } + + /** + * 获取CSV生成器(写出器),使用默认配置,覆盖已有文件(如果存在) + * + * @param file File CSV文件 + * @param charset 编码 + * @return {@link CsvWriter} + */ + public static CsvWriter getWriter(File file, Charset charset) { + return new CsvWriter(file, charset); + } + + /** + * 获取CSV生成器(写出器),使用默认配置 + * + * @param filePath File CSV文件路径 + * @param charset 编码 + * @param isAppend 是否追加 + * @return {@link CsvWriter} + */ + public static CsvWriter getWriter(String filePath, Charset charset, boolean isAppend) { + return new CsvWriter(filePath, charset, isAppend); + } + + /** + * 获取CSV生成器(写出器),使用默认配置 + * + * @param file File CSV文件 + * @param charset 编码 + * @param isAppend 是否追加 + * @return {@link CsvWriter} + */ + public static CsvWriter getWriter(File file, Charset charset, boolean isAppend) { + return new CsvWriter(file, charset, isAppend); + } + + /** + * 获取CSV生成器(写出器) + * + * @param file File CSV文件 + * @param charset 编码 + * @param isAppend 是否追加 + * @param config 写出配置,null则使用默认配置 + * @return {@link CsvWriter} + */ + public static CsvWriter getWriter(File file, Charset charset, boolean isAppend, CsvWriteConfig config) { + return new CsvWriter(file, charset, isAppend, config); + } + + /** + * 获取CSV生成器(写出器) + * + * @param writer Writer + * @return {@link CsvWriter} + */ + public static CsvWriter getWriter(Writer writer) { + return new CsvWriter(writer); + } + + /** + * 获取CSV生成器(写出器) + * + * @param writer Writer + * @param config 写出配置,null则使用默认配置 + * @return {@link CsvWriter} + */ + public static CsvWriter getWriter(Writer writer, CsvWriteConfig config) { + return new CsvWriter(writer, config); + } +} diff --git a/src/main/java/cn/hutool/core/text/csv/CsvWriteConfig.java b/src/main/java/cn/hutool/core/text/csv/CsvWriteConfig.java new file mode 100644 index 0000000..eff5b64 --- /dev/null +++ b/src/main/java/cn/hutool/core/text/csv/CsvWriteConfig.java @@ -0,0 +1,54 @@ +package cn.hutool.core.text.csv; + +import cn.hutool.core.util.CharUtil; + +import java.io.Serializable; + +/** + * CSV写出配置项 + * + * @author looly + */ +public class CsvWriteConfig extends CsvConfig implements Serializable { + private static final long serialVersionUID = 5396453565371560052L; + + /** + * 是否始终使用文本分隔符,文本包装符,默认false,按需添加 + */ + protected boolean alwaysDelimitText; + /** + * 换行符 + */ + protected char[] lineDelimiter = {CharUtil.CR, CharUtil.LF}; + + /** + * 默认配置 + * + * @return 默认配置 + */ + public static CsvWriteConfig defaultConfig() { + return new CsvWriteConfig(); + } + + /** + * 设置是否始终使用文本分隔符,文本包装符,默认false,按需添加 + * + * @param alwaysDelimitText 是否始终使用文本分隔符,文本包装符,默认false,按需添加 + * @return this + */ + public CsvWriteConfig setAlwaysDelimitText(boolean alwaysDelimitText) { + this.alwaysDelimitText = alwaysDelimitText; + return this; + } + + /** + * 设置换行符 + * + * @param lineDelimiter 换行符 + * @return this + */ + public CsvWriteConfig setLineDelimiter(char[] lineDelimiter) { + this.lineDelimiter = lineDelimiter; + return this; + } +} diff --git a/src/main/java/cn/hutool/core/text/csv/CsvWriter.java b/src/main/java/cn/hutool/core/text/csv/CsvWriter.java new file mode 100644 index 0000000..3edb3d4 --- /dev/null +++ b/src/main/java/cn/hutool/core/text/csv/CsvWriter.java @@ -0,0 +1,451 @@ +package cn.hutool.core.text.csv; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.collection.ArrayIter; +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.convert.Convert; +import cn.hutool.core.io.FileUtil; +import cn.hutool.core.io.IORuntimeException; +import cn.hutool.core.io.IoUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.CharUtil; +import cn.hutool.core.util.CharsetUtil; +import cn.hutool.core.util.ObjectUtil; + +import java.io.BufferedWriter; +import java.io.Closeable; +import java.io.File; +import java.io.Flushable; +import java.io.IOException; +import java.io.Serializable; +import java.io.Writer; +import java.nio.charset.Charset; +import java.util.List; +import java.util.Map; + +/** + * CSV数据写出器 + * + * @author Looly + * @since 4.0.5 + */ +public final class CsvWriter implements Closeable, Flushable, Serializable { + private static final long serialVersionUID = 1L; + + /** + * 写出器 + */ + private final Writer writer; + /** + * 写出配置 + */ + private final CsvWriteConfig config; + /** + * 是否处于新行开始 + */ + private boolean newline = true; + /** + * 是否首行,即CSV开始的位置,当初始化时默认为true,一旦写入内容,为false + */ + private boolean isFirstLine = true; + + // --------------------------------------------------------------------------------------------------- Constructor start + + /** + * 构造,覆盖已有文件(如果存在),默认编码UTF-8 + * + * @param filePath File CSV文件路径 + */ + public CsvWriter(String filePath) { + this(FileUtil.file(filePath)); + } + + /** + * 构造,覆盖已有文件(如果存在),默认编码UTF-8 + * + * @param file File CSV文件 + */ + public CsvWriter(File file) { + this(file, CharsetUtil.CHARSET_UTF_8); + } + + /** + * 构造,覆盖已有文件(如果存在) + * + * @param filePath File CSV文件路径 + * @param charset 编码 + */ + public CsvWriter(String filePath, Charset charset) { + this(FileUtil.file(filePath), charset); + } + + /** + * 构造,覆盖已有文件(如果存在) + * + * @param file File CSV文件 + * @param charset 编码 + */ + public CsvWriter(File file, Charset charset) { + this(file, charset, false); + } + + /** + * 构造 + * + * @param filePath File CSV文件路径 + * @param charset 编码 + * @param isAppend 是否追加 + */ + public CsvWriter(String filePath, Charset charset, boolean isAppend) { + this(FileUtil.file(filePath), charset, isAppend); + } + + /** + * 构造 + * + * @param file CSV文件 + * @param charset 编码 + * @param isAppend 是否追加 + */ + public CsvWriter(File file, Charset charset, boolean isAppend) { + this(file, charset, isAppend, null); + } + + /** + * 构造 + * + * @param filePath CSV文件路径 + * @param charset 编码 + * @param isAppend 是否追加 + * @param config 写出配置,null则使用默认配置 + */ + public CsvWriter(String filePath, Charset charset, boolean isAppend, CsvWriteConfig config) { + this(FileUtil.file(filePath), charset, isAppend, config); + } + + /** + * 构造 + * + * @param file CSV文件 + * @param charset 编码 + * @param isAppend 是否追加 + * @param config 写出配置,null则使用默认配置 + */ + public CsvWriter(File file, Charset charset, boolean isAppend, CsvWriteConfig config) { + this(FileUtil.getWriter(file, charset, isAppend), config); + } + + /** + * 构造,使用默认配置 + * + * @param writer {@link Writer} + */ + public CsvWriter(Writer writer) { + this(writer, null); + } + + /** + * 构造 + * + * @param writer Writer + * @param config 写出配置,null则使用默认配置 + */ + public CsvWriter(Writer writer, CsvWriteConfig config) { + this.writer = (writer instanceof BufferedWriter) ? writer : new BufferedWriter(writer); + this.config = ObjectUtil.defaultIfNull(config, CsvWriteConfig::defaultConfig); + } + // --------------------------------------------------------------------------------------------------- Constructor end + + /** + * 设置是否始终使用文本分隔符,文本包装符,默认false,按需添加 + * + * @param alwaysDelimitText 是否始终使用文本分隔符,文本包装符,默认false,按需添加 + * @return this + */ + public CsvWriter setAlwaysDelimitText(boolean alwaysDelimitText) { + this.config.setAlwaysDelimitText(alwaysDelimitText); + return this; + } + + /** + * 设置换行符 + * + * @param lineDelimiter 换行符 + * @return this + */ + public CsvWriter setLineDelimiter(char[] lineDelimiter) { + this.config.setLineDelimiter(lineDelimiter); + return this; + } + + /** + * 将多行写出到Writer + * + * @param lines 多行数据 + * @return this + * @throws IORuntimeException IO异常 + */ + public CsvWriter write(String[]... lines) throws IORuntimeException { + return write(new ArrayIter<>(lines)); + } + + /** + * 将多行写出到Writer + * + * @param lines 多行数据,每行数据可以是集合或者数组 + * @return this + * @throws IORuntimeException IO异常 + */ + public CsvWriter write(Iterable lines) throws IORuntimeException { + if (CollUtil.isNotEmpty(lines)) { + for (Object values : lines) { + appendLine(Convert.toStrArray(values)); + } + flush(); + } + return this; + } + + /** + * 将一个 CsvData 集合写出到Writer + * + * @param csvData CsvData + * @return this + * @since 5.7.4 + */ + public CsvWriter write(CsvData csvData) { + if (csvData != null) { + // 1、写header + final List header = csvData.getHeader(); + if (CollUtil.isNotEmpty(header)) { + this.writeHeaderLine(header.toArray(new String[0])); + } + // 2、写内容 + this.write(csvData.getRows()); + flush(); + } + return this; + } + + /** + * 将一个Bean集合写出到Writer,并自动生成表头 + * + * @param beans Bean集合 + * @return this + */ + public CsvWriter writeBeans(Iterable beans) { + if (CollUtil.isNotEmpty(beans)) { + boolean isFirst = true; + Map map; + for (Object bean : beans) { + map = BeanUtil.beanToMap(bean); + if (isFirst) { + writeHeaderLine(map.keySet().toArray(new String[0])); + isFirst = false; + } + writeLine(Convert.toStrArray(map.values())); + } + flush(); + } + return this; + } + + /** + * 写出一行头部行,支持标题别名 + * + * @param fields 字段列表 ({@code null} 值会被做为空值追加 + * @return this + * @throws IORuntimeException IO异常 + * @since 5.7.10 + */ + public CsvWriter writeHeaderLine(String... fields) throws IORuntimeException { + final Map headerAlias = this.config.headerAlias; + if (MapUtil.isNotEmpty(headerAlias)) { + // 标题别名替换 + String alias; + for (int i = 0; i < fields.length; i++) { + alias = headerAlias.get(fields[i]); + if (null != alias) { + fields[i] = alias; + } + } + } + return writeLine(fields); + } + + /** + * 写出一行 + * + * @param fields 字段列表 ({@code null} 值会被做为空值追加) + * @return this + * @throws IORuntimeException IO异常 + * @since 5.5.7 + */ + public CsvWriter writeLine(String... fields) throws IORuntimeException { + if (ArrayUtil.isEmpty(fields)) { + return writeLine(); + } + appendLine(fields); + return this; + } + + /** + * 追加新行(换行) + * + * @return this + * @throws IORuntimeException IO异常 + */ + public CsvWriter writeLine() throws IORuntimeException { + try { + writer.write(config.lineDelimiter); + } catch (IOException e) { + throw new IORuntimeException(e); + } + newline = true; + return this; + } + + /** + * 写出一行注释,注释符号可自定义
+ * 如果注释符不存在,则抛出异常 + * + * @param comment 注释内容 + * @return this + * @see CsvConfig#commentCharacter + * @since 5.5.7 + */ + public CsvWriter writeComment(String comment) { + Assert.notNull(this.config.commentCharacter, "Comment is disable!"); + try { + if(isFirstLine){ + // 首行不补换行符 + isFirstLine = false; + }else { + writer.write(config.lineDelimiter); + } + writer.write(this.config.commentCharacter); + writer.write(comment); + newline = true; + } catch (IOException e) { + throw new IORuntimeException(e); + } + return this; + } + + @Override + public void close() { + IoUtil.close(this.writer); + } + + @Override + public void flush() throws IORuntimeException { + try { + writer.flush(); + } catch (IOException e) { + throw new IORuntimeException(e); + } + } + + // --------------------------------------------------------------------------------------------------- Private method start + + /** + * 追加一行,末尾会自动换行,但是追加前不会换行 + * + * @param fields 字段列表 ({@code null} 值会被做为空值追加) + * @throws IORuntimeException IO异常 + */ + private void appendLine(String... fields) throws IORuntimeException { + try { + doAppendLine(fields); + } catch (IOException e) { + throw new IORuntimeException(e); + } + } + + /** + * 追加一行,末尾会自动换行,但是追加前不会换行 + * + * @param fields 字段列表 ({@code null} 值会被做为空值追加) + * @throws IOException IO异常 + */ + private void doAppendLine(String... fields) throws IOException { + if (null != fields) { + if(isFirstLine){ + // 首行不补换行符 + isFirstLine = false; + }else { + writer.write(config.lineDelimiter); + } + for (String field : fields) { + appendField(field); + } + newline = true; + } + } + + /** + * 在当前行追加字段值,自动添加字段分隔符,如果有必要,自动包装字段 + * + * @param value 字段值,{@code null} 会被做为空串写出 + * @throws IOException IO异常 + */ + private void appendField(final String value) throws IOException { + boolean alwaysDelimitText = config.alwaysDelimitText; + char textDelimiter = config.textDelimiter; + char fieldSeparator = config.fieldSeparator; + + if (!newline) { + writer.write(fieldSeparator); + } else { + newline = false; + } + + if (null == value) { + if (alwaysDelimitText) { + writer.write(new char[]{textDelimiter, textDelimiter}); + } + return; + } + + final char[] valueChars = value.toCharArray(); + boolean needsTextDelimiter = alwaysDelimitText; + boolean containsTextDelimiter = false; + + for (final char c : valueChars) { + if (c == textDelimiter) { + // 字段值中存在包装符 + containsTextDelimiter = needsTextDelimiter = true; + break; + } else if (c == fieldSeparator || c == CharUtil.LF || c == CharUtil.CR) { + // 包含分隔符或换行符需要包装符包装 + needsTextDelimiter = true; + } + } + + // 包装符开始 + if (needsTextDelimiter) { + writer.write(textDelimiter); + } + + // 正文 + if (containsTextDelimiter) { + for (final char c : valueChars) { + // 转义文本包装符 + if (c == textDelimiter) { + writer.write(textDelimiter); + } + writer.write(c); + } + } else { + writer.write(valueChars); + } + + // 包装符结尾 + if (needsTextDelimiter) { + writer.write(textDelimiter); + } + } + // --------------------------------------------------------------------------------------------------- Private method end +} diff --git a/src/main/java/cn/hutool/core/text/csv/package-info.java b/src/main/java/cn/hutool/core/text/csv/package-info.java new file mode 100644 index 0000000..7863d5c --- /dev/null +++ b/src/main/java/cn/hutool/core/text/csv/package-info.java @@ -0,0 +1,8 @@ +/** + * 提供CSV文件读写的封装,入口为CsvUtil
+ * 规范见:https://datatracker.ietf.org/doc/html/rfc4180 + * + * @author looly + * + */ +package cn.hutool.core.text.csv; diff --git a/src/main/java/cn/hutool/core/text/escape/Html4Escape.java b/src/main/java/cn/hutool/core/text/escape/Html4Escape.java new file mode 100644 index 0000000..023ab98 --- /dev/null +++ b/src/main/java/cn/hutool/core/text/escape/Html4Escape.java @@ -0,0 +1,316 @@ +package cn.hutool.core.text.escape; + +import cn.hutool.core.text.replacer.LookupReplacer; + +/** + * HTML4的ESCAPE + * 参考:Commons Lang3 + * + * @author looly + * + */ +public class Html4Escape extends XmlEscape { + private static final long serialVersionUID = 1L; + + protected static final String[][] ISO8859_1_ESCAPE = { // + { "\u00A0", " " }, // non-breaking space + { "\u00A1", "¡" }, // inverted exclamation mark + { "\u00A2", "¢" }, // cent sign + { "\u00A3", "£" }, // pound sign + { "\u00A4", "¤" }, // currency sign + { "\u00A5", "¥" }, // yen sign = yuan sign + { "\u00A6", "¦" }, // broken bar = broken vertical bar + { "\u00A7", "§" }, // section sign + { "\u00A8", "¨" }, // diaeresis = spacing diaeresis + { "\u00A9", "©" }, // � - copyright sign + { "\u00AA", "ª" }, // feminine ordinal indicator + { "\u00AB", "«" }, // left-pointing double angle quotation mark = left pointing guillemet + { "\u00AC", "¬" }, // not sign + { "\u00AD", "­" }, // soft hyphen = discretionary hyphen + { "\u00AE", "®" }, // � - registered trademark sign + { "\u00AF", "¯" }, // macron = spacing macron = overline = APL overbar + { "\u00B0", "°" }, // degree sign + { "\u00B1", "±" }, // plus-minus sign = plus-or-minus sign + { "\u00B2", "²" }, // superscript two = superscript digit two = squared + { "\u00B3", "³" }, // superscript three = superscript digit three = cubed + { "\u00B4", "´" }, // acute accent = spacing acute + { "\u00B5", "µ" }, // micro sign + { "\u00B6", "¶" }, // pilcrow sign = paragraph sign + { "\u00B7", "·" }, // middle dot = Georgian comma = Greek middle dot + { "\u00B8", "¸" }, // cedilla = spacing cedilla + { "\u00B9", "¹" }, // superscript one = superscript digit one + { "\u00BA", "º" }, // masculine ordinal indicator + { "\u00BB", "»" }, // right-pointing double angle quotation mark = right pointing guillemet + { "\u00BC", "¼" }, // vulgar fraction one quarter = fraction one quarter + { "\u00BD", "½" }, // vulgar fraction one half = fraction one half + { "\u00BE", "¾" }, // vulgar fraction three quarters = fraction three quarters + { "\u00BF", "¿" }, // inverted question mark = turned question mark + { "\u00C0", "À" }, // � - uppercase A, grave accent + { "\u00C1", "Á" }, // � - uppercase A, acute accent + { "\u00C2", "Â" }, // � - uppercase A, circumflex accent + { "\u00C3", "Ã" }, // � - uppercase A, tilde + { "\u00C4", "Ä" }, // � - uppercase A, umlaut + { "\u00C5", "Å" }, // � - uppercase A, ring + { "\u00C6", "Æ" }, // � - uppercase AE + { "\u00C7", "Ç" }, // � - uppercase C, cedilla + { "\u00C8", "È" }, // � - uppercase E, grave accent + { "\u00C9", "É" }, // � - uppercase E, acute accent + { "\u00CA", "Ê" }, // � - uppercase E, circumflex accent + { "\u00CB", "Ë" }, // � - uppercase E, umlaut + { "\u00CC", "Ì" }, // � - uppercase I, grave accent + { "\u00CD", "Í" }, // � - uppercase I, acute accent + { "\u00CE", "Î" }, // � - uppercase I, circumflex accent + { "\u00CF", "Ï" }, // � - uppercase I, umlaut + { "\u00D0", "Ð" }, // � - uppercase Eth, Icelandic + { "\u00D1", "Ñ" }, // � - uppercase N, tilde + { "\u00D2", "Ò" }, // � - uppercase O, grave accent + { "\u00D3", "Ó" }, // � - uppercase O, acute accent + { "\u00D4", "Ô" }, // � - uppercase O, circumflex accent + { "\u00D5", "Õ" }, // � - uppercase O, tilde + { "\u00D6", "Ö" }, // � - uppercase O, umlaut + { "\u00D7", "×" }, // multiplication sign + { "\u00D8", "Ø" }, // � - uppercase O, slash + { "\u00D9", "Ù" }, // � - uppercase U, grave accent + { "\u00DA", "Ú" }, // � - uppercase U, acute accent + { "\u00DB", "Û" }, // � - uppercase U, circumflex accent + { "\u00DC", "Ü" }, // � - uppercase U, umlaut + { "\u00DD", "Ý" }, // � - uppercase Y, acute accent + { "\u00DE", "Þ" }, // � - uppercase THORN, Icelandic + { "\u00DF", "ß" }, // � - lowercase sharps, German + { "\u00E0", "à" }, // � - lowercase a, grave accent + { "\u00E1", "á" }, // � - lowercase a, acute accent + { "\u00E2", "â" }, // � - lowercase a, circumflex accent + { "\u00E3", "ã" }, // � - lowercase a, tilde + { "\u00E4", "ä" }, // � - lowercase a, umlaut + { "\u00E5", "å" }, // � - lowercase a, ring + { "\u00E6", "æ" }, // � - lowercase ae + { "\u00E7", "ç" }, // � - lowercase c, cedilla + { "\u00E8", "è" }, // � - lowercase e, grave accent + { "\u00E9", "é" }, // � - lowercase e, acute accent + { "\u00EA", "ê" }, // � - lowercase e, circumflex accent + { "\u00EB", "ë" }, // � - lowercase e, umlaut + { "\u00EC", "ì" }, // � - lowercase i, grave accent + { "\u00ED", "í" }, // � - lowercase i, acute accent + { "\u00EE", "î" }, // � - lowercase i, circumflex accent + { "\u00EF", "ï" }, // � - lowercase i, umlaut + { "\u00F0", "ð" }, // � - lowercase eth, Icelandic + { "\u00F1", "ñ" }, // � - lowercase n, tilde + { "\u00F2", "ò" }, // � - lowercase o, grave accent + { "\u00F3", "ó" }, // � - lowercase o, acute accent + { "\u00F4", "ô" }, // � - lowercase o, circumflex accent + { "\u00F5", "õ" }, // � - lowercase o, tilde + { "\u00F6", "ö" }, // � - lowercase o, umlaut + { "\u00F7", "÷" }, // division sign + { "\u00F8", "ø" }, // � - lowercase o, slash + { "\u00F9", "ù" }, // � - lowercase u, grave accent + { "\u00FA", "ú" }, // � - lowercase u, acute accent + { "\u00FB", "û" }, // � - lowercase u, circumflex accent + { "\u00FC", "ü" }, // � - lowercase u, umlaut + { "\u00FD", "ý" }, // � - lowercase y, acute accent + { "\u00FE", "þ" }, // � - lowercase thorn, Icelandic + { "\u00FF", "ÿ" }, // � - lowercase y, umlaut + }; + + protected static final String[][] HTML40_EXTENDED_ESCAPE = { + // + { "\u0192", "ƒ" }, // latin small f with hook = function= florin, U+0192 ISOtech --> + // + { "\u0391", "Α" }, // greek capital letter alpha, U+0391 --> + { "\u0392", "Β" }, // greek capital letter beta, U+0392 --> + { "\u0393", "Γ" }, // greek capital letter gamma,U+0393 ISOgrk3 --> + { "\u0394", "Δ" }, // greek capital letter delta,U+0394 ISOgrk3 --> + { "\u0395", "Ε" }, // greek capital letter epsilon, U+0395 --> + { "\u0396", "Ζ" }, // greek capital letter zeta, U+0396 --> + { "\u0397", "Η" }, // greek capital letter eta, U+0397 --> + { "\u0398", "Θ" }, // greek capital letter theta,U+0398 ISOgrk3 --> + { "\u0399", "Ι" }, // greek capital letter iota, U+0399 --> + { "\u039A", "Κ" }, // greek capital letter kappa, U+039A --> + { "\u039B", "Λ" }, // greek capital letter lambda,U+039B ISOgrk3 --> + { "\u039C", "Μ" }, // greek capital letter mu, U+039C --> + { "\u039D", "Ν" }, // greek capital letter nu, U+039D --> + { "\u039E", "Ξ" }, // greek capital letter xi, U+039E ISOgrk3 --> + { "\u039F", "Ο" }, // greek capital letter omicron, U+039F --> + { "\u03A0", "Π" }, // greek capital letter pi, U+03A0 ISOgrk3 --> + { "\u03A1", "Ρ" }, // greek capital letter rho, U+03A1 --> + // + { "\u03A3", "Σ" }, // greek capital letter sigma,U+03A3 ISOgrk3 --> + { "\u03A4", "Τ" }, // greek capital letter tau, U+03A4 --> + { "\u03A5", "Υ" }, // greek capital letter upsilon,U+03A5 ISOgrk3 --> + { "\u03A6", "Φ" }, // greek capital letter phi,U+03A6 ISOgrk3 --> + { "\u03A7", "Χ" }, // greek capital letter chi, U+03A7 --> + { "\u03A8", "Ψ" }, // greek capital letter psi,U+03A8 ISOgrk3 --> + { "\u03A9", "Ω" }, // greek capital letter omega,U+03A9 ISOgrk3 --> + { "\u03B1", "α" }, // greek small letter alpha,U+03B1 ISOgrk3 --> + { "\u03B2", "β" }, // greek small letter beta, U+03B2 ISOgrk3 --> + { "\u03B3", "γ" }, // greek small letter gamma,U+03B3 ISOgrk3 --> + { "\u03B4", "δ" }, // greek small letter delta,U+03B4 ISOgrk3 --> + { "\u03B5", "ε" }, // greek small letter epsilon,U+03B5 ISOgrk3 --> + { "\u03B6", "ζ" }, // greek small letter zeta, U+03B6 ISOgrk3 --> + { "\u03B7", "η" }, // greek small letter eta, U+03B7 ISOgrk3 --> + { "\u03B8", "θ" }, // greek small letter theta,U+03B8 ISOgrk3 --> + { "\u03B9", "ι" }, // greek small letter iota, U+03B9 ISOgrk3 --> + { "\u03BA", "κ" }, // greek small letter kappa,U+03BA ISOgrk3 --> + { "\u03BB", "λ" }, // greek small letter lambda,U+03BB ISOgrk3 --> + { "\u03BC", "μ" }, // greek small letter mu, U+03BC ISOgrk3 --> + { "\u03BD", "ν" }, // greek small letter nu, U+03BD ISOgrk3 --> + { "\u03BE", "ξ" }, // greek small letter xi, U+03BE ISOgrk3 --> + { "\u03BF", "ο" }, // greek small letter omicron, U+03BF NEW --> + { "\u03C0", "π" }, // greek small letter pi, U+03C0 ISOgrk3 --> + { "\u03C1", "ρ" }, // greek small letter rho, U+03C1 ISOgrk3 --> + { "\u03C2", "ς" }, // greek small letter final sigma,U+03C2 ISOgrk3 --> + { "\u03C3", "σ" }, // greek small letter sigma,U+03C3 ISOgrk3 --> + { "\u03C4", "τ" }, // greek small letter tau, U+03C4 ISOgrk3 --> + { "\u03C5", "υ" }, // greek small letter upsilon,U+03C5 ISOgrk3 --> + { "\u03C6", "φ" }, // greek small letter phi, U+03C6 ISOgrk3 --> + { "\u03C7", "χ" }, // greek small letter chi, U+03C7 ISOgrk3 --> + { "\u03C8", "ψ" }, // greek small letter psi, U+03C8 ISOgrk3 --> + { "\u03C9", "ω" }, // greek small letter omega,U+03C9 ISOgrk3 --> + { "\u03D1", "ϑ" }, // greek small letter theta symbol,U+03D1 NEW --> + { "\u03D2", "ϒ" }, // greek upsilon with hook symbol,U+03D2 NEW --> + { "\u03D6", "ϖ" }, // greek pi symbol, U+03D6 ISOgrk3 --> + // + { "\u2022", "•" }, // bullet = black small circle,U+2022 ISOpub --> + // + { "\u2026", "…" }, // horizontal ellipsis = three dot leader,U+2026 ISOpub --> + { "\u2032", "′" }, // prime = minutes = feet, U+2032 ISOtech --> + { "\u2033", "″" }, // double prime = seconds = inches,U+2033 ISOtech --> + { "\u203E", "‾" }, // overline = spacing overscore,U+203E NEW --> + { "\u2044", "⁄" }, // fraction slash, U+2044 NEW --> + // + { "\u2118", "℘" }, // script capital P = power set= Weierstrass p, U+2118 ISOamso --> + { "\u2111", "ℑ" }, // blackletter capital I = imaginary part,U+2111 ISOamso --> + { "\u211C", "ℜ" }, // blackletter capital R = real part symbol,U+211C ISOamso --> + { "\u2122", "™" }, // trade mark sign, U+2122 ISOnum --> + { "\u2135", "ℵ" }, // alef symbol = first transfinite cardinal,U+2135 NEW --> + // + // + { "\u2190", "←" }, // leftwards arrow, U+2190 ISOnum --> + { "\u2191", "↑" }, // upwards arrow, U+2191 ISOnum--> + { "\u2192", "→" }, // rightwards arrow, U+2192 ISOnum --> + { "\u2193", "↓" }, // downwards arrow, U+2193 ISOnum --> + { "\u2194", "↔" }, // left right arrow, U+2194 ISOamsa --> + { "\u21B5", "↵" }, // downwards arrow with corner leftwards= carriage return, U+21B5 NEW --> + { "\u21D0", "⇐" }, // leftwards double arrow, U+21D0 ISOtech --> + // + { "\u21D1", "⇑" }, // upwards double arrow, U+21D1 ISOamsa --> + { "\u21D2", "⇒" }, // rightwards double arrow,U+21D2 ISOtech --> + // + { "\u21D3", "⇓" }, // downwards double arrow, U+21D3 ISOamsa --> + { "\u21D4", "⇔" }, // left right double arrow,U+21D4 ISOamsa --> + // + { "\u2200", "∀" }, // for all, U+2200 ISOtech --> + { "\u2202", "∂" }, // partial differential, U+2202 ISOtech --> + { "\u2203", "∃" }, // there exists, U+2203 ISOtech --> + { "\u2205", "∅" }, // empty set = null set = diameter,U+2205 ISOamso --> + { "\u2207", "∇" }, // nabla = backward difference,U+2207 ISOtech --> + { "\u2208", "∈" }, // element of, U+2208 ISOtech --> + { "\u2209", "∉" }, // not an element of, U+2209 ISOtech --> + { "\u220B", "∋" }, // contains as member, U+220B ISOtech --> + // + { "\u220F", "∏" }, // n-ary product = product sign,U+220F ISOamsb --> + // + { "\u2211", "∑" }, // n-ary summation, U+2211 ISOamsb --> + // + { "\u2212", "−" }, // minus sign, U+2212 ISOtech --> + { "\u2217", "∗" }, // asterisk operator, U+2217 ISOtech --> + { "\u221A", "√" }, // square root = radical sign,U+221A ISOtech --> + { "\u221D", "∝" }, // proportional to, U+221D ISOtech --> + { "\u221E", "∞" }, // infinity, U+221E ISOtech --> + { "\u2220", "∠" }, // angle, U+2220 ISOamso --> + { "\u2227", "∧" }, // logical and = wedge, U+2227 ISOtech --> + { "\u2228", "∨" }, // logical or = vee, U+2228 ISOtech --> + { "\u2229", "∩" }, // intersection = cap, U+2229 ISOtech --> + { "\u222A", "∪" }, // union = cup, U+222A ISOtech --> + { "\u222B", "∫" }, // integral, U+222B ISOtech --> + { "\u2234", "∴" }, // therefore, U+2234 ISOtech --> + { "\u223C", "∼" }, // tilde operator = varies with = similar to,U+223C ISOtech --> + // + { "\u2245", "≅" }, // approximately equal to, U+2245 ISOtech --> + { "\u2248", "≈" }, // almost equal to = asymptotic to,U+2248 ISOamsr --> + { "\u2260", "≠" }, // not equal to, U+2260 ISOtech --> + { "\u2261", "≡" }, // identical to, U+2261 ISOtech --> + { "\u2264", "≤" }, // less-than or equal to, U+2264 ISOtech --> + { "\u2265", "≥" }, // greater-than or equal to,U+2265 ISOtech --> + { "\u2282", "⊂" }, // subset of, U+2282 ISOtech --> + { "\u2283", "⊃" }, // superset of, U+2283 ISOtech --> + // + { "\u2286", "⊆" }, // subset of or equal to, U+2286 ISOtech --> + { "\u2287", "⊇" }, // superset of or equal to,U+2287 ISOtech --> + { "\u2295", "⊕" }, // circled plus = direct sum,U+2295 ISOamsb --> + { "\u2297", "⊗" }, // circled times = vector product,U+2297 ISOamsb --> + { "\u22A5", "⊥" }, // up tack = orthogonal to = perpendicular,U+22A5 ISOtech --> + { "\u22C5", "⋅" }, // dot operator, U+22C5 ISOamsb --> + // + // + { "\u2308", "⌈" }, // left ceiling = apl upstile,U+2308 ISOamsc --> + { "\u2309", "⌉" }, // right ceiling, U+2309 ISOamsc --> + { "\u230A", "⌊" }, // left floor = apl downstile,U+230A ISOamsc --> + { "\u230B", "⌋" }, // right floor, U+230B ISOamsc --> + { "\u2329", "⟨" }, // left-pointing angle bracket = bra,U+2329 ISOtech --> + // + { "\u232A", "⟩" }, // right-pointing angle bracket = ket,U+232A ISOtech --> + // + // + { "\u25CA", "◊" }, // lozenge, U+25CA ISOpub --> + // + { "\u2660", "♠" }, // black spade suit, U+2660 ISOpub --> + // + { "\u2663", "♣" }, // black club suit = shamrock,U+2663 ISOpub --> + { "\u2665", "♥" }, // black heart suit = valentine,U+2665 ISOpub --> + { "\u2666", "♦" }, // black diamond suit, U+2666 ISOpub --> + + // + { "\u0152", "Œ" }, // -- latin capital ligature OE,U+0152 ISOlat2 --> + { "\u0153", "œ" }, // -- latin small ligature oe, U+0153 ISOlat2 --> + // + { "\u0160", "Š" }, // -- latin capital letter S with caron,U+0160 ISOlat2 --> + { "\u0161", "š" }, // -- latin small letter s with caron,U+0161 ISOlat2 --> + { "\u0178", "Ÿ" }, // -- latin capital letter Y with diaeresis,U+0178 ISOlat2 --> + // + { "\u02C6", "ˆ" }, // -- modifier letter circumflex accent,U+02C6 ISOpub --> + { "\u02DC", "˜" }, // small tilde, U+02DC ISOdia --> + // + { "\u2002", " " }, // en space, U+2002 ISOpub --> + { "\u2003", " " }, // em space, U+2003 ISOpub --> + { "\u2009", " " }, // thin space, U+2009 ISOpub --> + { "\u200C", "‌" }, // zero width non-joiner,U+200C NEW RFC 2070 --> + { "\u200D", "‍" }, // zero width joiner, U+200D NEW RFC 2070 --> + { "\u200E", "‎" }, // left-to-right mark, U+200E NEW RFC 2070 --> + { "\u200F", "‏" }, // right-to-left mark, U+200F NEW RFC 2070 --> + { "\u2013", "–" }, // en dash, U+2013 ISOpub --> + { "\u2014", "—" }, // em dash, U+2014 ISOpub --> + { "\u2018", "‘" }, // left single quotation mark,U+2018 ISOnum --> + { "\u2019", "’" }, // right single quotation mark,U+2019 ISOnum --> + { "\u201A", "‚" }, // single low-9 quotation mark, U+201A NEW --> + { "\u201C", "“" }, // left double quotation mark,U+201C ISOnum --> + { "\u201D", "”" }, // right double quotation mark,U+201D ISOnum --> + { "\u201E", "„" }, // double low-9 quotation mark, U+201E NEW --> + { "\u2020", "†" }, // dagger, U+2020 ISOpub --> + { "\u2021", "‡" }, // double dagger, U+2021 ISOpub --> + { "\u2030", "‰" }, // per mille sign, U+2030 ISOtech --> + { "\u2039", "‹" }, // single left-pointing angle quotation mark,U+2039 ISO proposed --> + // + { "\u203A", "›" }, // single right-pointing angle quotation mark,U+203A ISO proposed --> + // + { "\u20AC", "€" }, // -- euro sign, U+20AC NEW --> + }; + + public Html4Escape() { + super(); + addChain(new LookupReplacer(ISO8859_1_ESCAPE)); + addChain(new LookupReplacer(HTML40_EXTENDED_ESCAPE)); + } +} diff --git a/src/main/java/cn/hutool/core/text/escape/Html4Unescape.java b/src/main/java/cn/hutool/core/text/escape/Html4Unescape.java new file mode 100644 index 0000000..0ac03c4 --- /dev/null +++ b/src/main/java/cn/hutool/core/text/escape/Html4Unescape.java @@ -0,0 +1,22 @@ +package cn.hutool.core.text.escape; + +import cn.hutool.core.text.replacer.LookupReplacer; + +/** + * HTML4的UNESCAPE + * + * @author looly + * + */ +public class Html4Unescape extends XmlUnescape { + private static final long serialVersionUID = 1L; + + protected static final String[][] ISO8859_1_UNESCAPE = InternalEscapeUtil.invert(Html4Escape.ISO8859_1_ESCAPE); + protected static final String[][] HTML40_EXTENDED_UNESCAPE = InternalEscapeUtil.invert(Html4Escape.HTML40_EXTENDED_ESCAPE); + + public Html4Unescape() { + super(); + addChain(new LookupReplacer(ISO8859_1_UNESCAPE)); + addChain(new LookupReplacer(HTML40_EXTENDED_UNESCAPE)); + } +} diff --git a/src/main/java/cn/hutool/core/text/escape/InternalEscapeUtil.java b/src/main/java/cn/hutool/core/text/escape/InternalEscapeUtil.java new file mode 100644 index 0000000..d318aef --- /dev/null +++ b/src/main/java/cn/hutool/core/text/escape/InternalEscapeUtil.java @@ -0,0 +1,24 @@ +package cn.hutool.core.text.escape; + +/** + * 内部Escape工具类 + * @author looly + * + */ +class InternalEscapeUtil { + + /** + * 将数组中的0和1位置的值互换,即键值转换 + * + * @param array String[][] 被转换的数组 + * @return String[][] 转换后的数组 + */ + public static String[][] invert(final String[][] array) { + final String[][] newarray = new String[array.length][2]; + for (int i = 0; i < array.length; i++) { + newarray[i][0] = array[i][1]; + newarray[i][1] = array[i][0]; + } + return newarray; + } +} diff --git a/src/main/java/cn/hutool/core/text/escape/NumericEntityUnescaper.java b/src/main/java/cn/hutool/core/text/escape/NumericEntityUnescaper.java new file mode 100644 index 0000000..7a649e7 --- /dev/null +++ b/src/main/java/cn/hutool/core/text/escape/NumericEntityUnescaper.java @@ -0,0 +1,56 @@ +package cn.hutool.core.text.escape; + +import cn.hutool.core.text.StrBuilder; +import cn.hutool.core.text.replacer.StrReplacer; +import cn.hutool.core.util.CharUtil; + +/** + * 形如'的反转义器 + * + * @author looly + * + */ +public class NumericEntityUnescaper extends StrReplacer { + private static final long serialVersionUID = 1L; + + @Override + protected int replace(CharSequence str, int pos, StrBuilder out) { + final int len = str.length(); + // 检查以确保以&#开头 + if (str.charAt(pos) == '&' && pos < len - 2 && str.charAt(pos + 1) == '#') { + int start = pos + 2; + boolean isHex = false; + final char firstChar = str.charAt(start); + if (firstChar == 'x' || firstChar == 'X') { + start++; + isHex = true; + } + + // 确保&#后还有数字 + if (start == len) { + return 0; + } + + int end = start; + while (end < len && CharUtil.isHexChar(str.charAt(end))) { + end++; + } + final boolean isSemiNext = (end != len) && (str.charAt(end) == ';'); + if (isSemiNext) { + int entityValue; + try { + if (isHex) { + entityValue = Integer.parseInt(str.subSequence(start, end).toString(), 16); + } else { + entityValue = Integer.parseInt(str.subSequence(start, end).toString(), 10); + } + } catch (final NumberFormatException nfe) { + return 0; + } + out.append((char)entityValue); + return 2 + end - start + (isHex ? 1 : 0) + 1; + } + } + return 0; + } +} diff --git a/src/main/java/cn/hutool/core/text/escape/XmlEscape.java b/src/main/java/cn/hutool/core/text/escape/XmlEscape.java new file mode 100644 index 0000000..8aba9e2 --- /dev/null +++ b/src/main/java/cn/hutool/core/text/escape/XmlEscape.java @@ -0,0 +1,38 @@ +package cn.hutool.core.text.escape; + +import cn.hutool.core.text.replacer.LookupReplacer; +import cn.hutool.core.text.replacer.ReplacerChain; + +/** + * XML特殊字符转义
+ * 见:https://stackoverflow.com/questions/1091945/what-characters-do-i-need-to-escape-in-xml-documents
+ * + *
+ * 	 & (ampersand) 替换为 &amp;
+ * 	 < (less than) 替换为 &lt;
+ * 	 > (greater than) 替换为 &gt;
+ * 	 " (double quote) 替换为 &quot;
+ * 	 ' (single quote / apostrophe) 替换为 &apos;
+ * 
+ * + * @author looly + * @since 5.7.2 + */ +public class XmlEscape extends ReplacerChain { + private static final long serialVersionUID = 1L; + + protected static final String[][] BASIC_ESCAPE = { // +// {"'", "'"}, // " - single-quote + {"\"", """}, // " - double-quote + {"&", "&"}, // & - ampersand + {"<", "<"}, // < - less-than + {">", ">"}, // > - greater-than + }; + + /** + * 构造 + */ + public XmlEscape() { + addChain(new LookupReplacer(BASIC_ESCAPE)); + } +} diff --git a/src/main/java/cn/hutool/core/text/escape/XmlUnescape.java b/src/main/java/cn/hutool/core/text/escape/XmlUnescape.java new file mode 100644 index 0000000..80073a9 --- /dev/null +++ b/src/main/java/cn/hutool/core/text/escape/XmlUnescape.java @@ -0,0 +1,27 @@ +package cn.hutool.core.text.escape; + +import cn.hutool.core.text.replacer.LookupReplacer; +import cn.hutool.core.text.replacer.ReplacerChain; + +/** + * XML的UNESCAPE + * + * @author looly + * @since 5.7.2 + */ +public class XmlUnescape extends ReplacerChain { + private static final long serialVersionUID = 1L; + + protected static final String[][] BASIC_UNESCAPE = InternalEscapeUtil.invert(XmlEscape.BASIC_ESCAPE); + // issue#1118 + protected static final String[][] OTHER_UNESCAPE = new String[][]{new String[]{"'", "'"}}; + + /** + * 构造 + */ + public XmlUnescape() { + addChain(new LookupReplacer(BASIC_UNESCAPE)); + addChain(new NumericEntityUnescaper()); + addChain(new LookupReplacer(OTHER_UNESCAPE)); + } +} diff --git a/src/main/java/cn/hutool/core/text/escape/package-info.java b/src/main/java/cn/hutool/core/text/escape/package-info.java new file mode 100644 index 0000000..fb3e702 --- /dev/null +++ b/src/main/java/cn/hutool/core/text/escape/package-info.java @@ -0,0 +1,7 @@ +/** + * 提供各种转义和反转义实现 + * + * @author looly + * + */ +package cn.hutool.core.text.escape; \ No newline at end of file diff --git a/src/main/java/cn/hutool/core/text/finder/CharFinder.java b/src/main/java/cn/hutool/core/text/finder/CharFinder.java new file mode 100644 index 0000000..4328581 --- /dev/null +++ b/src/main/java/cn/hutool/core/text/finder/CharFinder.java @@ -0,0 +1,66 @@ +package cn.hutool.core.text.finder; + +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.NumberUtil; + +/** + * 字符查找器
+ * 查找指定字符在字符串中的位置信息 + * + * @author looly + * @since 5.7.14 + */ +public class CharFinder extends TextFinder { + private static final long serialVersionUID = 1L; + + private final char c; + private final boolean caseInsensitive; + + /** + * 构造,不忽略字符大小写 + * + * @param c 被查找的字符 + */ + public CharFinder(char c) { + this(c, false); + } + + /** + * 构造 + * + * @param c 被查找的字符 + * @param caseInsensitive 是否忽略大小写 + */ + public CharFinder(char c, boolean caseInsensitive) { + this.c = c; + this.caseInsensitive = caseInsensitive; + } + + @Override + public int start(int from) { + Assert.notNull(this.text, "Text to find must be not null!"); + final int limit = getValidEndIndex(); + if(negative){ + for (int i = from; i > limit; i--) { + if (NumberUtil.equals(c, text.charAt(i), caseInsensitive)) { + return i; + } + } + } else{ + for (int i = from; i < limit; i++) { + if (NumberUtil.equals(c, text.charAt(i), caseInsensitive)) { + return i; + } + } + } + return -1; + } + + @Override + public int end(int start) { + if (start < 0) { + return -1; + } + return start + 1; + } +} diff --git a/src/main/java/cn/hutool/core/text/finder/CharMatcherFinder.java b/src/main/java/cn/hutool/core/text/finder/CharMatcherFinder.java new file mode 100644 index 0000000..216d68e --- /dev/null +++ b/src/main/java/cn/hutool/core/text/finder/CharMatcherFinder.java @@ -0,0 +1,53 @@ +package cn.hutool.core.text.finder; + +import cn.hutool.core.lang.Assert; +import cn.hutool.core.lang.Matcher; + +/** + * 字符匹配查找器
+ * 查找满足指定{@link Matcher} 匹配的字符所在位置,此类长用于查找某一类字符,如数字等 + * + * @since 5.7.14 + * @author looly + */ +public class CharMatcherFinder extends TextFinder { + private static final long serialVersionUID = 1L; + + private final Matcher matcher; + + /** + * 构造 + * @param matcher 被查找的字符匹配器 + */ + public CharMatcherFinder(Matcher matcher) { + this.matcher = matcher; + } + + @Override + public int start(int from) { + Assert.notNull(this.text, "Text to find must be not null!"); + final int limit = getValidEndIndex(); + if(negative){ + for (int i = from; i > limit; i--) { + if(matcher.match(text.charAt(i))){ + return i; + } + } + } else { + for (int i = from; i < limit; i++) { + if(matcher.match(text.charAt(i))){ + return i; + } + } + } + return -1; + } + + @Override + public int end(int start) { + if(start < 0){ + return -1; + } + return start + 1; + } +} diff --git a/src/main/java/cn/hutool/core/text/finder/Finder.java b/src/main/java/cn/hutool/core/text/finder/Finder.java new file mode 100644 index 0000000..f6a1aa0 --- /dev/null +++ b/src/main/java/cn/hutool/core/text/finder/Finder.java @@ -0,0 +1,36 @@ +package cn.hutool.core.text.finder; + +/** + * 字符串查找接口,通过调用{@link #start(int)}查找开始位置,再调用{@link #end(int)}找结束位置 + * + * @author looly + * @since 5.7.14 + */ +public interface Finder { + + int INDEX_NOT_FOUND = -1; + + /** + * 返回开始位置,即起始字符位置(包含),未找到返回-1 + * + * @param from 查找的开始位置(包含) + * @return 起始字符位置,未找到返回-1 + */ + int start(int from); + + /** + * 返回结束位置,即最后一个字符后的位置(不包含) + * + * @param start 找到的起始位置 + * @return 结束位置,未找到返回-1 + */ + int end(int start); + + /** + * 复位查找器,用于重用对象 + * @return this + */ + default Finder reset(){ + return this; + } +} diff --git a/src/main/java/cn/hutool/core/text/finder/LengthFinder.java b/src/main/java/cn/hutool/core/text/finder/LengthFinder.java new file mode 100644 index 0000000..026bb4d --- /dev/null +++ b/src/main/java/cn/hutool/core/text/finder/LengthFinder.java @@ -0,0 +1,49 @@ +package cn.hutool.core.text.finder; + +import cn.hutool.core.lang.Assert; + +/** + * 固定长度查找器
+ * 给定一个长度,查找的位置为from + length,一般用于分段截取 + * + * @since 5.7.14 + * @author looly + */ +public class LengthFinder extends TextFinder { + private static final long serialVersionUID = 1L; + + private final int length; + + /** + * 构造 + * @param length 长度 + */ + public LengthFinder(int length) { + Assert.isTrue(length > 0, "Length must be great than 0"); + this.length = length; + } + + @Override + public int start(int from) { + Assert.notNull(this.text, "Text to find must be not null!"); + final int limit = getValidEndIndex(); + int result; + if(negative){ + result = from - length; + if(result > limit){ + return result; + } + } else { + result = from + length; + if(result < limit){ + return result; + } + } + return -1; + } + + @Override + public int end(int start) { + return start; + } +} diff --git a/src/main/java/cn/hutool/core/text/finder/PatternFinder.java b/src/main/java/cn/hutool/core/text/finder/PatternFinder.java new file mode 100644 index 0000000..c258dae --- /dev/null +++ b/src/main/java/cn/hutool/core/text/finder/PatternFinder.java @@ -0,0 +1,77 @@ +package cn.hutool.core.text.finder; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * 正则查找器
+ * 通过传入正则表达式,查找指定字符串中匹配正则的开始和结束位置 + * + * @author looly + * @since 5.7.14 + */ +public class PatternFinder extends TextFinder { + private static final long serialVersionUID = 1L; + + private final Pattern pattern; + private Matcher matcher; + + /** + * 构造 + * + * @param regex 被查找的正则表达式 + * @param caseInsensitive 是否忽略大小写 + */ + public PatternFinder(String regex, boolean caseInsensitive) { + this(Pattern.compile(regex, caseInsensitive ? Pattern.CASE_INSENSITIVE : 0)); + } + + /** + * 构造 + * + * @param pattern 被查找的正则{@link Pattern} + */ + public PatternFinder(Pattern pattern) { + this.pattern = pattern; + } + + @Override + public TextFinder setText(CharSequence text) { + this.matcher = pattern.matcher(text); + return super.setText(text); + } + + @Override + public TextFinder setNegative(boolean negative) { + throw new UnsupportedOperationException("Negative is invalid for Pattern!"); + } + + @Override + public int start(int from) { + if (matcher.find(from)) { + // 只有匹配到的字符串结尾在limit范围内,才算找到 + if(matcher.end() <= getValidEndIndex()){ + return matcher.start(); + } + } + return INDEX_NOT_FOUND; + } + + @Override + public int end(int start) { + final int end = matcher.end(); + final int limit; + if(endIndex < 0){ + limit = text.length(); + }else{ + limit = Math.min(endIndex, text.length()); + } + return end <= limit ? end : INDEX_NOT_FOUND; + } + + @Override + public PatternFinder reset() { + this.matcher.reset(); + return this; + } +} diff --git a/src/main/java/cn/hutool/core/text/finder/StrFinder.java b/src/main/java/cn/hutool/core/text/finder/StrFinder.java new file mode 100644 index 0000000..edf931d --- /dev/null +++ b/src/main/java/cn/hutool/core/text/finder/StrFinder.java @@ -0,0 +1,64 @@ +package cn.hutool.core.text.finder; + +import cn.hutool.core.lang.Assert; +import cn.hutool.core.text.CharSequenceUtil; + +/** + * 字符串查找器 + * + * @author looly + * @since 5.7.14 + */ +public class StrFinder extends TextFinder { + private static final long serialVersionUID = 1L; + + private final CharSequence strToFind; + private final boolean caseInsensitive; + + /** + * 构造 + * + * @param strToFind 被查找的字符串 + * @param caseInsensitive 是否忽略大小写 + */ + public StrFinder(CharSequence strToFind, boolean caseInsensitive) { + Assert.notEmpty(strToFind); + this.strToFind = strToFind; + this.caseInsensitive = caseInsensitive; + } + + @Override + public int start(int from) { + Assert.notNull(this.text, "Text to find must be not null!"); + final int subLen = strToFind.length(); + + if (from < 0) { + from = 0; + } + int endLimit = getValidEndIndex(); + if (negative) { + for (int i = from; i > endLimit; i--) { + if (CharSequenceUtil.isSubEquals(text, i, strToFind, 0, subLen, caseInsensitive)) { + return i; + } + } + } else { + endLimit = endLimit - subLen + 1; + for (int i = from; i < endLimit; i++) { + if (CharSequenceUtil.isSubEquals(text, i, strToFind, 0, subLen, caseInsensitive)) { + return i; + } + } + } + + return INDEX_NOT_FOUND; + } + + @Override + public int end(int start) { + if (start < 0) { + return -1; + } + return start + strToFind.length(); + } +} diff --git a/src/main/java/cn/hutool/core/text/finder/TextFinder.java b/src/main/java/cn/hutool/core/text/finder/TextFinder.java new file mode 100644 index 0000000..f2a5f9b --- /dev/null +++ b/src/main/java/cn/hutool/core/text/finder/TextFinder.java @@ -0,0 +1,74 @@ +package cn.hutool.core.text.finder; + +import cn.hutool.core.lang.Assert; + +import java.io.Serializable; + +/** + * 文本查找抽象类 + * + * @author looly + * @since 5.7.14 + */ +public abstract class TextFinder implements Finder, Serializable { + private static final long serialVersionUID = 1L; + + protected CharSequence text; + protected int endIndex = -1; + protected boolean negative; + + /** + * 设置被查找的文本 + * + * @param text 文本 + * @return this + */ + public TextFinder setText(CharSequence text) { + this.text = Assert.notNull(text, "Text must be not null!"); + return this; + } + + /** + * 设置查找的结束位置
+ * 如果从前向后查找,结束位置最大为text.length()
+ * 如果从后向前,结束位置为-1 + * + * @param endIndex 结束位置(不包括) + * @return this + */ + public TextFinder setEndIndex(int endIndex) { + this.endIndex = endIndex; + return this; + } + + /** + * 设置是否反向查找,{@code true}表示从后向前查找 + * + * @param negative 结束位置(不包括) + * @return this + */ + public TextFinder setNegative(boolean negative) { + this.negative = negative; + return this; + } + + /** + * 获取有效结束位置
+ * 如果{@link #endIndex}小于0,在反向模式下是开头(-1),正向模式是结尾(text.length()) + * + * @return 有效结束位置 + */ + protected int getValidEndIndex() { + if(negative && -1 == endIndex){ + // 反向查找模式下,-1表示0前面的位置,即字符串反向末尾的位置 + return -1; + } + final int limit; + if (endIndex < 0) { + limit = endIndex + text.length() + 1; + } else { + limit = Math.min(endIndex, text.length()); + } + return limit; + } +} diff --git a/src/main/java/cn/hutool/core/text/finder/package-info.java b/src/main/java/cn/hutool/core/text/finder/package-info.java new file mode 100644 index 0000000..98fccb9 --- /dev/null +++ b/src/main/java/cn/hutool/core/text/finder/package-info.java @@ -0,0 +1,13 @@ +/** + * 文本查找实现,包括: + *
    + *
  • 查找文本中的字符(正向、反向)
  • + *
  • 查找文本中的匹配字符(正向、反向)
  • + *
  • 查找文本中的字符串(正向、反向)
  • + *
  • 查找文本中匹配正则的字符串(正向)
  • + *
+ * + * @author looly + * + */ +package cn.hutool.core.text.finder; diff --git a/src/main/java/cn/hutool/core/text/package-info.java b/src/main/java/cn/hutool/core/text/package-info.java new file mode 100644 index 0000000..de673c1 --- /dev/null +++ b/src/main/java/cn/hutool/core/text/package-info.java @@ -0,0 +1,7 @@ +/** + * 提供文本相关操作的封装,还包括Unicode工具UnicodeUtil + * + * @author looly + * + */ +package cn.hutool.core.text; \ No newline at end of file diff --git a/src/main/java/cn/hutool/core/text/replacer/LookupReplacer.java b/src/main/java/cn/hutool/core/text/replacer/LookupReplacer.java new file mode 100644 index 0000000..ac3725a --- /dev/null +++ b/src/main/java/cn/hutool/core/text/replacer/LookupReplacer.java @@ -0,0 +1,74 @@ +package cn.hutool.core.text.replacer; + +import cn.hutool.core.text.StrBuilder; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +/** + * 查找替换器,通过查找指定关键字,替换对应的值 + * + * @author looly + * @since 4.1.5 + */ +public class LookupReplacer extends StrReplacer { + private static final long serialVersionUID = 1L; + + private final Map lookupMap; + private final Set prefixSet; + private final int minLength; + private final int maxLength; + + /** + * 构造 + * + * @param lookup 被查找的键值对 + */ + public LookupReplacer(String[]... lookup) { + this.lookupMap = new HashMap<>(); + this.prefixSet = new HashSet<>(); + + int minLength = Integer.MAX_VALUE; + int maxLength = 0; + String key; + int keySize; + for (String[] pair : lookup) { + key = pair[0]; + lookupMap.put(key, pair[1]); + this.prefixSet.add(key.charAt(0)); + keySize = key.length(); + if (keySize > maxLength) { + maxLength = keySize; + } + if (keySize < minLength) { + minLength = keySize; + } + } + this.maxLength = maxLength; + this.minLength = minLength; + } + + @Override + protected int replace(CharSequence str, int pos, StrBuilder out) { + if (prefixSet.contains(str.charAt(pos))) { + int max = this.maxLength; + if (pos + this.maxLength > str.length()) { + max = str.length() - pos; + } + CharSequence subSeq; + String result; + for (int i = max; i >= this.minLength; i--) { + subSeq = str.subSequence(pos, pos + i); + result = lookupMap.get(subSeq.toString()); + if(null != result) { + out.append(result); + return i; + } + } + } + return 0; + } + +} diff --git a/src/main/java/cn/hutool/core/text/replacer/ReplacerChain.java b/src/main/java/cn/hutool/core/text/replacer/ReplacerChain.java new file mode 100644 index 0000000..987bd00 --- /dev/null +++ b/src/main/java/cn/hutool/core/text/replacer/ReplacerChain.java @@ -0,0 +1,56 @@ +package cn.hutool.core.text.replacer; + +import cn.hutool.core.lang.Chain; +import cn.hutool.core.text.StrBuilder; + +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; + +/** + * 字符串替换链,用于组合多个字符串替换逻辑 + * + * @author looly + * @since 4.1.5 + */ +public class ReplacerChain extends StrReplacer implements Chain { + private static final long serialVersionUID = 1L; + + private final List replacers = new LinkedList<>(); + + /** + * 构造 + * + * @param strReplacers 字符串替换器 + */ + public ReplacerChain(StrReplacer... strReplacers) { + for (StrReplacer strReplacer : strReplacers) { + addChain(strReplacer); + } + } + + @SuppressWarnings("NullableProblems") + @Override + public Iterator iterator() { + return replacers.iterator(); + } + + @Override + public ReplacerChain addChain(StrReplacer element) { + replacers.add(element); + return this; + } + + @Override + protected int replace(CharSequence str, int pos, StrBuilder out) { + int consumed = 0; + for (StrReplacer strReplacer : replacers) { + consumed = strReplacer.replace(str, pos, out); + if (0 != consumed) { + return consumed; + } + } + return consumed; + } + +} diff --git a/src/main/java/cn/hutool/core/text/replacer/StrReplacer.java b/src/main/java/cn/hutool/core/text/replacer/StrReplacer.java new file mode 100644 index 0000000..fc2fabe --- /dev/null +++ b/src/main/java/cn/hutool/core/text/replacer/StrReplacer.java @@ -0,0 +1,45 @@ +package cn.hutool.core.text.replacer; + +import cn.hutool.core.lang.Replacer; +import cn.hutool.core.text.StrBuilder; + +import java.io.Serializable; + +/** + * 抽象字符串替换类
+ * 通过实现replace方法实现局部替换逻辑 + * + * @author looly + * @since 4.1.5 + */ +public abstract class StrReplacer implements Replacer, Serializable { + private static final long serialVersionUID = 1L; + + /** + * 抽象的字符串替换方法,通过传入原字符串和当前位置,执行替换逻辑,返回处理或替换的字符串长度部分。 + * + * @param str 被处理的字符串 + * @param pos 当前位置 + * @param out 输出 + * @return 处理的原字符串长度,0表示跳过此字符 + */ + protected abstract int replace(CharSequence str, int pos, StrBuilder out); + + @Override + public CharSequence replace(CharSequence t) { + final int len = t.length(); + final StrBuilder builder = StrBuilder.create(len); + int pos = 0;//当前位置 + int consumed;//处理过的字符数 + while (pos < len) { + consumed = replace(t, pos, builder); + if (0 == consumed) { + //0表示未处理或替换任何字符,原样输出本字符并从下一个字符继续 + builder.append(t.charAt(pos)); + pos++; + } + pos += consumed; + } + return builder; + } +} diff --git a/src/main/java/cn/hutool/core/text/replacer/package-info.java b/src/main/java/cn/hutool/core/text/replacer/package-info.java new file mode 100644 index 0000000..1362fc7 --- /dev/null +++ b/src/main/java/cn/hutool/core/text/replacer/package-info.java @@ -0,0 +1,7 @@ +/** + * 文本替换类抽象及实现 + * + * @author looly + * + */ +package cn.hutool.core.text.replacer; \ No newline at end of file diff --git a/src/main/java/cn/hutool/core/text/split/SplitIter.java b/src/main/java/cn/hutool/core/text/split/SplitIter.java new file mode 100644 index 0000000..da781a9 --- /dev/null +++ b/src/main/java/cn/hutool/core/text/split/SplitIter.java @@ -0,0 +1,153 @@ +package cn.hutool.core.text.split; + +import cn.hutool.core.collection.ComputeIter; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.text.finder.TextFinder; +import cn.hutool.core.util.StrUtil; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Function; + +/** + * 字符串切分迭代器
+ * 此迭代器是字符串切分的懒模式实现,实例化后不完成切分,只有调用{@link #hasNext()}或遍历时才完成切分
+ * 此迭代器非线程安全 + * + * @author looly + * @since 5.7.14 + */ +public class SplitIter extends ComputeIter implements Serializable { + private static final long serialVersionUID = 1L; + + private final String text; + private final TextFinder finder; + private final int limit; + private final boolean ignoreEmpty; + + /** + * 上一次的结束位置 + */ + private int offset; + /** + * 计数器,用于判断是否超过limit + */ + private int count; + + /** + * 构造 + * + * @param text 文本,不能为{@code null} + * @param separatorFinder 分隔符匹配器 + * @param limit 限制数量,小于等于0表示无限制 + * @param ignoreEmpty 是否忽略"" + */ + public SplitIter(CharSequence text, TextFinder separatorFinder, int limit, boolean ignoreEmpty) { + Assert.notNull(text, "Text must be not null!"); + this.text = text.toString(); + this.finder = separatorFinder.setText(text); + this.limit = limit > 0 ? limit : Integer.MAX_VALUE; + this.ignoreEmpty = ignoreEmpty; + } + + @Override + protected String computeNext() { + // 达到数量上限或末尾,结束 + if (count >= limit || offset > text.length()) { + return null; + } + + // 达到数量上限 + if (count == (limit - 1)) { + // 当到达限制次数时,最后一个元素为剩余部分 + if (ignoreEmpty && offset == text.length()) { + // 最后一个是空串 + return null; + } + + // 结尾整个作为一个元素 + count++; + return text.substring(offset); + } + + final int start = finder.start(offset); + // 无分隔符,结束 + if (start < 0) { + // 如果不再有分隔符,但是遗留了字符,则单独作为一个段 + if (offset <= text.length()) { + final String result = text.substring(offset); + if (!ignoreEmpty || !result.isEmpty()) { + // 返回非空串 + offset = Integer.MAX_VALUE; + return result; + } + } + return null; + } + + // 找到新的分隔符位置 + final String result = text.substring(offset, start); + offset = finder.end(start); + + if (ignoreEmpty && result.isEmpty()) { + // 发现空串且需要忽略时,跳过之 + return computeNext(); + } + + count++; + return result; + } + + /** + * 重置 + */ + public void reset() { + this.finder.reset(); + this.offset = 0; + this.count = 0; + } + + /** + * 获取切分后的对象数组 + * + * @param trim 是否去除元素两边空格 + * @return 切分后的列表 + */ + public String[] toArray(boolean trim) { + return toList(trim).toArray(new String[0]); + } + + /** + * 获取切分后的对象列表 + * + * @param trim 是否去除元素两边空格 + * @return 切分后的列表 + */ + public List toList(boolean trim) { + return toList((str) -> trim ? StrUtil.trim(str) : str); + } + + /** + * 获取切分后的对象列表 + * + * @param 元素类型 + * @param mapping 字符串映射函数 + * @return 切分后的列表 + */ + public List toList(Function mapping) { + final List result = new ArrayList<>(); + while (this.hasNext()) { + final T apply = mapping.apply(this.next()); + if (ignoreEmpty && StrUtil.isEmptyIfStr(apply)) { + // 对于mapping之后依旧是String的情况,ignoreEmpty依旧有效 + continue; + } + result.add(apply); + } + if (result.isEmpty()) { + return new ArrayList<>(0); + } + return result; + } +} diff --git a/src/main/java/cn/hutool/core/thread/AsyncUtil.java b/src/main/java/cn/hutool/core/thread/AsyncUtil.java new file mode 100644 index 0000000..ab99477 --- /dev/null +++ b/src/main/java/cn/hutool/core/thread/AsyncUtil.java @@ -0,0 +1,63 @@ +package cn.hutool.core.thread; + +import java.lang.reflect.UndeclaredThrowableException; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; + +/** + * {@link CompletableFuture}异步工具类
+ * {@link CompletableFuture} 是 Future 的改进,可以通过传入回调对象,在任务完成后调用之 + * + * @author achao1441470436@gmail.com + * @since 5.7.17 + */ +public class AsyncUtil { + + /** + * 等待所有任务执行完毕,包裹了异常 + * + * @param tasks 并行任务 + * @throws UndeclaredThrowableException 未受检异常 + */ + public static void waitAll(CompletableFuture... tasks) { + try { + CompletableFuture.allOf(tasks).get(); + } catch (InterruptedException | ExecutionException e) { + throw new ThreadException(e); + } + } + + /** + * 等待任意一个任务执行完毕,包裹了异常 + * + * @param 任务返回值类型 + * @param tasks 并行任务 + * @return 执行结束的任务返回值 + * @throws UndeclaredThrowableException 未受检异常 + */ + @SuppressWarnings("unchecked") + public static T waitAny(CompletableFuture... tasks) { + try { + return (T) CompletableFuture.anyOf(tasks).get(); + } catch (InterruptedException | ExecutionException e) { + throw new ThreadException(e); + } + } + + /** + * 获取异步任务结果,包裹了异常 + * + * @param 任务返回值类型 + * @param task 异步任务 + * @return 任务返回值 + * @throws RuntimeException 未受检异常 + */ + public static T get(CompletableFuture task) { + try { + return task.get(); + } catch (InterruptedException | ExecutionException e) { + throw new ThreadException(e); + } + } + +} diff --git a/src/main/java/cn/hutool/core/thread/BlockPolicy.java b/src/main/java/cn/hutool/core/thread/BlockPolicy.java new file mode 100644 index 0000000..0f9558d --- /dev/null +++ b/src/main/java/cn/hutool/core/thread/BlockPolicy.java @@ -0,0 +1,56 @@ +package cn.hutool.core.thread; + +import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.RejectedExecutionHandler; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.function.Consumer; + +/** + * 当任务队列过长时处于阻塞状态,直到添加到队列中 + * 如果阻塞过程中被中断,就会抛出{@link InterruptedException}异常
+ * 有时候在线程池内访问第三方接口,只希望固定并发数去访问,并且不希望丢弃任务时使用此策略,队列满的时候会处于阻塞状态(例如刷库的场景) + * + * @author luozongle + * @since 5.8.0 + */ +public class BlockPolicy implements RejectedExecutionHandler { + + /** + * 线程池关闭时,为避免任务丢失,留下处理方法 + * 如果需要由调用方来运行,可以{@code new BlockPolicy(Runnable::run)} + */ + private final Consumer handlerwhenshutdown; + + /** + * 构造 + * + * @param handlerwhenshutdown 线程池关闭后的执行策略 + */ + public BlockPolicy(final Consumer handlerwhenshutdown) { + this.handlerwhenshutdown = handlerwhenshutdown; + } + + /** + * 构造 + */ + public BlockPolicy() { + this(null); + } + + @Override + public void rejectedExecution(Runnable r, ThreadPoolExecutor e) { + // 线程池未关闭时,阻塞等待 + if (!e.isShutdown()) { + try { + e.getQueue().put(r); + } catch (InterruptedException ex) { + throw new RejectedExecutionException("Task " + r + " rejected from " + e); + } + } else if (null != handlerwhenshutdown) { + // 当设置了关闭时候的处理 + handlerwhenshutdown.accept(r); + } + + // 线程池关闭后,丢弃任务 + } +} diff --git a/src/main/java/cn/hutool/core/thread/ConcurrencyTester.java b/src/main/java/cn/hutool/core/thread/ConcurrencyTester.java new file mode 100644 index 0000000..f0cdd33 --- /dev/null +++ b/src/main/java/cn/hutool/core/thread/ConcurrencyTester.java @@ -0,0 +1,89 @@ +package cn.hutool.core.thread; + +import cn.hutool.core.date.TimeInterval; + +import java.io.Closeable; +import java.io.IOException; + +/** + * 高并发测试工具类 + * + *
+ * ps:
+ * //模拟1000个线程并发
+ * ConcurrencyTester ct = new ConcurrencyTester(1000);
+ * ct.test(() -> {
+ *      // 需要并发测试的业务代码
+ * });
+ *
+ * Console.log(ct.getInterval());
+ * ct.close();
+ * 
+ * + * @author kwer + */ +public class ConcurrencyTester implements Closeable { + private final SyncFinisher sf; + private final TimeInterval timeInterval; + private long interval; + + /** + * 构造 + * @param threadSize 线程数 + */ + public ConcurrencyTester(int threadSize) { + this.sf = new SyncFinisher(threadSize); + this.timeInterval = new TimeInterval(); + } + + /** + * 执行测试
+ * 执行测试后不会关闭线程池,可以调用{@link #close()}释放线程池 + * + * @param runnable 要测试的内容 + * @return this + */ + public ConcurrencyTester test(Runnable runnable) { + this.sf.clearWorker(); + + timeInterval.start(); + this.sf + .addRepeatWorker(runnable) + .setBeginAtSameTime(true) + .start(); + + this.interval = timeInterval.interval(); + return this; + } + + /** + * 重置测试器,重置包括: + * + *
    + *
  • 清空worker
  • + *
  • 重置计时器
  • + *
+ * + * @return this + * @since 5.7.2 + */ + public ConcurrencyTester reset(){ + this.sf.clearWorker(); + this.timeInterval.restart(); + return this; + } + + /** + * 获取执行时间 + * + * @return 执行时间,单位毫秒 + */ + public long getInterval() { + return this.interval; + } + + @Override + public void close() throws IOException { + this.sf.close(); + } +} diff --git a/src/main/java/cn/hutool/core/thread/DelegatedExecutorService.java b/src/main/java/cn/hutool/core/thread/DelegatedExecutorService.java new file mode 100644 index 0000000..85e740b --- /dev/null +++ b/src/main/java/cn/hutool/core/thread/DelegatedExecutorService.java @@ -0,0 +1,100 @@ +package cn.hutool.core.thread; + +import cn.hutool.core.lang.Assert; + +import java.util.Collection; +import java.util.List; +import java.util.concurrent.AbstractExecutorService; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +/** + * ExecutorService代理 + * + * @author loolly + */ +public class DelegatedExecutorService extends AbstractExecutorService { + private final ExecutorService e; + + /** + * 构造 + * + * @param executor {@link ExecutorService} + */ + DelegatedExecutorService(ExecutorService executor) { + Assert.notNull(executor, "executor must be not null !"); + e = executor; + } + + @Override + public void execute(Runnable command) { + e.execute(command); + } + + @Override + public void shutdown() { + e.shutdown(); + } + + @Override + public List shutdownNow() { + return e.shutdownNow(); + } + + @Override + public boolean isShutdown() { + return e.isShutdown(); + } + + @Override + public boolean isTerminated() { + return e.isTerminated(); + } + + @Override + public boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException { + return e.awaitTermination(timeout, unit); + } + + @Override + public Future submit(Runnable task) { + return e.submit(task); + } + + @Override + public Future submit(Callable task) { + return e.submit(task); + } + + @Override + public Future submit(Runnable task, T result) { + return e.submit(task, result); + } + + @Override + public List> invokeAll(Collection> tasks) throws InterruptedException { + return e.invokeAll(tasks); + } + + @Override + public List> invokeAll(Collection> tasks, long timeout, TimeUnit unit) + throws InterruptedException { + return e.invokeAll(tasks, timeout, unit); + } + + @Override + public T invokeAny(Collection> tasks) + throws InterruptedException, ExecutionException { + return e.invokeAny(tasks); + } + + @Override + public T invokeAny(Collection> tasks, long timeout, TimeUnit unit) + throws InterruptedException, ExecutionException, TimeoutException { + return e.invokeAny(tasks, timeout, unit); + } +} diff --git a/src/main/java/cn/hutool/core/thread/ExecutorBuilder.java b/src/main/java/cn/hutool/core/thread/ExecutorBuilder.java new file mode 100644 index 0000000..ccb9c86 --- /dev/null +++ b/src/main/java/cn/hutool/core/thread/ExecutorBuilder.java @@ -0,0 +1,260 @@ +package cn.hutool.core.thread; + +import cn.hutool.core.builder.Builder; +import cn.hutool.core.util.ObjectUtil; + +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.RejectedExecutionHandler; +import java.util.concurrent.SynchronousQueue; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +/** + * {@link ThreadPoolExecutor} 建造者 + * + *
+ *     1. 如果池中任务数 < corePoolSize     -》 放入立即执行
+ *     2. 如果池中任务数 > corePoolSize     -》 放入队列等待
+ *     3. 队列满                              -》 新建线程立即执行
+ *     4. 执行中的线程 > maxPoolSize        -》 触发handler(RejectedExecutionHandler)异常
+ * 
+ * + * @author looly + * @since 4.1.9 + */ +public class ExecutorBuilder implements Builder { + private static final long serialVersionUID = 1L; + + /** 默认的等待队列容量 */ + public static final int DEFAULT_QUEUE_CAPACITY = 1024; + + /** + * 初始池大小 + */ + private int corePoolSize; + /** + * 最大池大小(允许同时执行的最大线程数) + */ + private int maxPoolSize = Integer.MAX_VALUE; + /** + * 线程存活时间,即当池中线程多于初始大小时,多出的线程保留的时长 + */ + private long keepAliveTime = TimeUnit.SECONDS.toNanos(60); + /** + * 队列,用于存放未执行的线程 + */ + private BlockingQueue workQueue; + /** + * 线程工厂,用于自定义线程创建 + */ + private ThreadFactory threadFactory; + /** + * 当线程阻塞(block)时的异常处理器,所谓线程阻塞即线程池和等待队列已满,无法处理线程时采取的策略 + */ + private RejectedExecutionHandler handler; + /** + * 线程执行超时后是否回收线程 + */ + private Boolean allowCoreThreadTimeOut; + + /** + * 设置初始池大小,默认0 + * + * @param corePoolSize 初始池大小 + * @return this + */ + public ExecutorBuilder setCorePoolSize(int corePoolSize) { + this.corePoolSize = corePoolSize; + return this; + } + + /** + * 设置最大池大小(允许同时执行的最大线程数) + * + * @param maxPoolSize 最大池大小(允许同时执行的最大线程数) + * @return this + */ + public ExecutorBuilder setMaxPoolSize(int maxPoolSize) { + this.maxPoolSize = maxPoolSize; + return this; + } + + /** + * 设置线程存活时间,即当池中线程多于初始大小时,多出的线程保留的时长 + * + * @param keepAliveTime 线程存活时间 + * @param unit 单位 + * @return this + */ + public ExecutorBuilder setKeepAliveTime(long keepAliveTime, TimeUnit unit) { + return setKeepAliveTime(unit.toNanos(keepAliveTime)); + } + + /** + * 设置线程存活时间,即当池中线程多于初始大小时,多出的线程保留的时长,单位纳秒 + * + * @param keepAliveTime 线程存活时间,单位纳秒 + * @return this + */ + public ExecutorBuilder setKeepAliveTime(long keepAliveTime) { + this.keepAliveTime = keepAliveTime; + return this; + } + + /** + * 设置队列,用于存在未执行的线程
+ * 可选队列有: + * + *
+	 * 1. {@link SynchronousQueue}    它将任务直接提交给线程而不保持它们。当运行线程小于maxPoolSize时会创建新线程,否则触发异常策略
+	 * 2. {@link LinkedBlockingQueue} 默认无界队列,当运行线程大于corePoolSize时始终放入此队列,此时maxPoolSize无效。
+	 *                        当构造LinkedBlockingQueue对象时传入参数,变为有界队列,队列满时,运行线程小于maxPoolSize时会创建新线程,否则触发异常策略
+	 * 3. {@link ArrayBlockingQueue}  有界队列,相对无界队列有利于控制队列大小,队列满时,运行线程小于maxPoolSize时会创建新线程,否则触发异常策略
+	 * 
+ * + * @param workQueue 队列 + * @return this + */ + public ExecutorBuilder setWorkQueue(BlockingQueue workQueue) { + this.workQueue = workQueue; + return this; + } + + /** + * 使用{@link ArrayBlockingQueue} 做为等待队列
+ * 有界队列,相对无界队列有利于控制队列大小,队列满时,运行线程小于maxPoolSize时会创建新线程,否则触发异常策略 + * + * @param capacity 队列容量 + * @return this + * @since 5.1.4 + */ + public ExecutorBuilder useArrayBlockingQueue(int capacity) { + return setWorkQueue(new ArrayBlockingQueue<>(capacity)); + } + + /** + * 使用{@link SynchronousQueue} 做为等待队列(非公平策略)
+ * 它将任务直接提交给线程而不保持它们。当运行线程小于maxPoolSize时会创建新线程,否则触发异常策略 + * + * @return this + * @since 4.1.11 + */ + public ExecutorBuilder useSynchronousQueue() { + return useSynchronousQueue(false); + } + + /** + * 使用{@link SynchronousQueue} 做为等待队列
+ * 它将任务直接提交给线程而不保持它们。当运行线程小于maxPoolSize时会创建新线程,否则触发异常策略 + * + * @param fair 是否使用公平访问策略 + * @return this + * @since 4.5.0 + */ + public ExecutorBuilder useSynchronousQueue(boolean fair) { + return setWorkQueue(new SynchronousQueue<>(fair)); + } + + /** + * 设置线程工厂,用于自定义线程创建 + * + * @param threadFactory 线程工厂 + * @return this + * @see ThreadFactoryBuilder + */ + public ExecutorBuilder setThreadFactory(ThreadFactory threadFactory) { + this.threadFactory = threadFactory; + return this; + } + + /** + * 设置当线程阻塞(block)时的异常处理器,所谓线程阻塞即线程池和等待队列已满,无法处理线程时采取的策略 + *

+ * 此处可以使用JDK预定义的几种策略,见{@link RejectPolicy}枚举 + * + * @param handler {@link RejectedExecutionHandler} + * @return this + * @see RejectPolicy + */ + public ExecutorBuilder setHandler(RejectedExecutionHandler handler) { + this.handler = handler; + return this; + } + + /** + * 设置线程执行超时后是否回收线程 + * + * @param allowCoreThreadTimeOut 线程执行超时后是否回收线程 + * @return this + */ + public ExecutorBuilder setAllowCoreThreadTimeOut(boolean allowCoreThreadTimeOut) { + this.allowCoreThreadTimeOut = allowCoreThreadTimeOut; + return this; + } + + /** + * 创建ExecutorBuilder,开始构建 + * + * @return this + */ + public static ExecutorBuilder create() { + return new ExecutorBuilder(); + } + + /** + * 构建ThreadPoolExecutor + */ + @Override + public ThreadPoolExecutor build() { + return build(this); + } + + /** + * 创建有回收关闭功能的ExecutorService + * + * @return 创建有回收关闭功能的ExecutorService + * @since 5.1.4 + */ + public ExecutorService buildFinalizable() { + return new FinalizableDelegatedExecutorService(build()); + } + + /** + * 构建ThreadPoolExecutor + * + * @param builder this + * @return {@link ThreadPoolExecutor} + */ + private static ThreadPoolExecutor build(ExecutorBuilder builder) { + final int corePoolSize = builder.corePoolSize; + final int maxPoolSize = builder.maxPoolSize; + final long keepAliveTime = builder.keepAliveTime; + final BlockingQueue workQueue; + if (null != builder.workQueue) { + workQueue = builder.workQueue; + } else { + // corePoolSize为0则要使用SynchronousQueue避免无限阻塞 + workQueue = (corePoolSize <= 0) ? new SynchronousQueue<>() : new LinkedBlockingQueue<>(DEFAULT_QUEUE_CAPACITY); + } + final ThreadFactory threadFactory = (null != builder.threadFactory) ? builder.threadFactory : Executors.defaultThreadFactory(); + RejectedExecutionHandler handler = ObjectUtil.defaultIfNull(builder.handler, RejectPolicy.ABORT.getValue()); + + final ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(// + corePoolSize, // + maxPoolSize, // + keepAliveTime, TimeUnit.NANOSECONDS, // + workQueue, // + threadFactory, // + handler// + ); + if (null != builder.allowCoreThreadTimeOut) { + threadPoolExecutor.allowCoreThreadTimeOut(builder.allowCoreThreadTimeOut); + } + return threadPoolExecutor; + } +} diff --git a/src/main/java/cn/hutool/core/thread/FinalizableDelegatedExecutorService.java b/src/main/java/cn/hutool/core/thread/FinalizableDelegatedExecutorService.java new file mode 100644 index 0000000..40c930e --- /dev/null +++ b/src/main/java/cn/hutool/core/thread/FinalizableDelegatedExecutorService.java @@ -0,0 +1,25 @@ +package cn.hutool.core.thread; + +import java.util.concurrent.ExecutorService; + +/** + * 保证ExecutorService在对象回收时正常结束 + * + * @author loolly + */ +public class FinalizableDelegatedExecutorService extends DelegatedExecutorService { + + /** + * 构造 + * + * @param executor {@link ExecutorService} + */ + FinalizableDelegatedExecutorService(ExecutorService executor) { + super(executor); + } + + @Override + protected void finalize() { + super.shutdown(); + } +} \ No newline at end of file diff --git a/src/main/java/cn/hutool/core/thread/GlobalThreadPool.java b/src/main/java/cn/hutool/core/thread/GlobalThreadPool.java new file mode 100644 index 0000000..3293376 --- /dev/null +++ b/src/main/java/cn/hutool/core/thread/GlobalThreadPool.java @@ -0,0 +1,96 @@ +package cn.hutool.core.thread; + +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Future; + +import cn.hutool.core.exceptions.UtilException; + +/** + * 全局公共线程池
+ * 此线程池是一个无限线程池,即加入的线程不等待任何线程,直接执行 + * + * @author Looly + * + */ +public class GlobalThreadPool { + private static ExecutorService executor; + + private GlobalThreadPool() { + } + + static { + init(); + } + + /** + * 初始化全局线程池 + */ + synchronized public static void init() { + if (null != executor) { + executor.shutdownNow(); + } + executor = ExecutorBuilder.create().useSynchronousQueue().build(); + } + + /** + * 关闭公共线程池 + * + * @param isNow 是否立即关闭而不等待正在执行的线程 + */ + synchronized public static void shutdown(boolean isNow) { + if (null != executor) { + if (isNow) { + executor.shutdownNow(); + } else { + executor.shutdown(); + } + } + } + + /** + * 获得 {@link ExecutorService} + * + * @return {@link ExecutorService} + */ + public static ExecutorService getExecutor() { + return executor; + } + + /** + * 直接在公共线程池中执行线程 + * + * @param runnable 可运行对象 + */ + public static void execute(Runnable runnable) { + try { + executor.execute(runnable); + } catch (Exception e) { + throw new UtilException(e, "Exception when running task!"); + } + } + + /** + * 执行有返回值的异步方法
+ * Future代表一个异步执行的操作,通过get()方法可以获得操作的结果,如果异步操作还没有完成,则,get()会使当前线程阻塞 + * + * @param 执行的Task + * @param task {@link Callable} + * @return Future + */ + public static Future submit(Callable task) { + return executor.submit(task); + } + + /** + * 执行有返回值的异步方法
+ * Future代表一个异步执行的操作,通过get()方法可以获得操作的结果,如果异步操作还没有完成,则,get()会使当前线程阻塞 + * + * @param runnable 可运行对象 + * @return {@link Future} + * @since 3.0.5 + */ + public static Future submit(Runnable runnable) { + return executor.submit(runnable); + } +} diff --git a/src/main/java/cn/hutool/core/thread/NamedThreadFactory.java b/src/main/java/cn/hutool/core/thread/NamedThreadFactory.java new file mode 100644 index 0000000..1239ac0 --- /dev/null +++ b/src/main/java/cn/hutool/core/thread/NamedThreadFactory.java @@ -0,0 +1,98 @@ +package cn.hutool.core.thread; + +import java.lang.Thread.UncaughtExceptionHandler; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.atomic.AtomicInteger; + +import cn.hutool.core.util.StrUtil; + +/** + * 线程创建工厂类,此工厂可选配置: + * + *

+ * 1. 自定义线程命名前缀
+ * 2. 自定义是否守护线程
+ * 
+ * + * @author looly + * @since 4.0.0 + */ +public class NamedThreadFactory implements ThreadFactory { + + /** 命名前缀 */ + private final String prefix; + /** 线程组 */ + private final ThreadGroup group; + /** 线程组 */ + private final AtomicInteger threadNumber = new AtomicInteger(1); + /** 是否守护线程 */ + private final boolean isDaemon; + /** 无法捕获的异常统一处理 */ + private final UncaughtExceptionHandler handler; + + /** + * 构造 + * + * @param prefix 线程名前缀 + * @param isDaemon 是否守护线程 + */ + public NamedThreadFactory(String prefix, boolean isDaemon) { + this(prefix, null, isDaemon); + } + + /** + * 构造 + * + * @param prefix 线程名前缀 + * @param threadGroup 线程组,可以为null + * @param isDaemon 是否守护线程 + */ + public NamedThreadFactory(String prefix, ThreadGroup threadGroup, boolean isDaemon) { + this(prefix, threadGroup, isDaemon, null); + } + + /** + * 构造 + * + * @param prefix 线程名前缀 + * @param threadGroup 线程组,可以为null + * @param isDaemon 是否守护线程 + * @param handler 未捕获异常处理 + */ + public NamedThreadFactory(String prefix, ThreadGroup threadGroup, boolean isDaemon, UncaughtExceptionHandler handler) { + this.prefix = StrUtil.isBlank(prefix) ? "Hutool" : prefix; + if (null == threadGroup) { + threadGroup = ThreadUtil.currentThreadGroup(); + } + this.group = threadGroup; + this.isDaemon = isDaemon; + this.handler = handler; + } + + @Override + public Thread newThread(Runnable r) { + final Thread t = new Thread(this.group, r, StrUtil.format("{}{}", prefix, threadNumber.getAndIncrement())); + + //守护线程 + if (!t.isDaemon()) { + if (isDaemon) { + // 原线程为非守护则设置为守护 + t.setDaemon(true); + } + } else if (!isDaemon) { + // 原线程为守护则还原为非守护 + t.setDaemon(false); + } + //异常处理 + if(null != this.handler) { + t.setUncaughtExceptionHandler(handler); + } + //优先级 + if (Thread.NORM_PRIORITY != t.getPriority()) { + // 标准优先级 + t.setPriority(Thread.NORM_PRIORITY); + } + return t; + } + +} diff --git a/src/main/java/cn/hutool/core/thread/RejectPolicy.java b/src/main/java/cn/hutool/core/thread/RejectPolicy.java new file mode 100644 index 0000000..36956ca --- /dev/null +++ b/src/main/java/cn/hutool/core/thread/RejectPolicy.java @@ -0,0 +1,42 @@ +package cn.hutool.core.thread; + +import java.util.concurrent.RejectedExecutionHandler; +import java.util.concurrent.ThreadPoolExecutor; + +/** + * 线程拒绝策略枚举 + * + *

+ * 如果设置了maxSize, 当总线程数达到上限, 会调用RejectedExecutionHandler进行处理,此枚举为JDK预定义的几种策略枚举表示 + * + * @author looly + * @since 4.1.13 + */ +public enum RejectPolicy { + + /** 处理程序遭到拒绝将抛出RejectedExecutionException */ + ABORT(new ThreadPoolExecutor.AbortPolicy()), + /** 放弃当前任务 */ + DISCARD(new ThreadPoolExecutor.DiscardPolicy()), + /** 如果执行程序尚未关闭,则位于工作队列头部的任务将被删除,然后重试执行程序(如果再次失败,则重复此过程) */ + DISCARD_OLDEST(new ThreadPoolExecutor.DiscardOldestPolicy()), + /** 由主线程来直接执行 */ + CALLER_RUNS(new ThreadPoolExecutor.CallerRunsPolicy()), + /** 当任务队列过长时处于阻塞状态,直到添加到队列中,固定并发数去访问,并且不希望丢弃任务时使用此策略 */ + BLOCK(new BlockPolicy()); + + private final RejectedExecutionHandler value; + + RejectPolicy(RejectedExecutionHandler handler) { + this.value = handler; + } + + /** + * 获取RejectedExecutionHandler枚举值 + * + * @return RejectedExecutionHandler + */ + public RejectedExecutionHandler getValue() { + return this.value; + } +} diff --git a/src/main/java/cn/hutool/core/thread/SemaphoreRunnable.java b/src/main/java/cn/hutool/core/thread/SemaphoreRunnable.java new file mode 100644 index 0000000..07caca7 --- /dev/null +++ b/src/main/java/cn/hutool/core/thread/SemaphoreRunnable.java @@ -0,0 +1,59 @@ +package cn.hutool.core.thread; + +import java.util.concurrent.Semaphore; + +/** + * 带有信号量控制的{@link Runnable} 接口抽象实现 + * + *

+ * 通过设置信号量,可以限制可以访问某些资源(物理或逻辑的)线程数目。
+ * 例如:设置信号量为2,表示最多有两个线程可以同时执行方法逻辑,其余线程等待,直到此线程逻辑执行完毕 + *

+ * + * @author looly + * @since 4.4.5 + */ +public class SemaphoreRunnable implements Runnable { + + /** 实际执行的逻辑 */ + private final Runnable runnable; + /** 信号量 */ + private final Semaphore semaphore; + + /** + * 构造 + * + * @param runnable 实际执行的线程逻辑 + * @param semaphore 信号量,多个线程必须共享同一信号量 + */ + public SemaphoreRunnable(Runnable runnable, Semaphore semaphore) { + this.runnable = runnable; + this.semaphore = semaphore; + } + + /** + * 获得信号量 + * + * @return {@link Semaphore} + * @since 5.3.6 + */ + public Semaphore getSemaphore(){ + return this.semaphore; + } + + @Override + public void run() { + if (null != this.semaphore) { + try{ + semaphore.acquire(); + try { + this.runnable.run(); + } finally { + semaphore.release(); + } + }catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + } +} diff --git a/src/main/java/cn/hutool/core/thread/SyncFinisher.java b/src/main/java/cn/hutool/core/thread/SyncFinisher.java new file mode 100644 index 0000000..a990fae --- /dev/null +++ b/src/main/java/cn/hutool/core/thread/SyncFinisher.java @@ -0,0 +1,232 @@ +package cn.hutool.core.thread; + +import cn.hutool.core.exceptions.UtilException; + +import java.io.Closeable; +import java.io.IOException; +import java.util.LinkedHashSet; +import java.util.Set; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; + +/** + * 线程同步结束器
+ * 在完成一组正在其他线程中执行的操作之前,它允许一个或多个线程一直等待。 + * + *
+ * ps:
+ * //模拟1000个线程并发
+ * SyncFinisher sf = new SyncFinisher(1000);
+ * sf.addWorker(() -> {
+ *      // 需要并发测试的业务代码
+ * });
+ * sf.start()
+ * 
+ * + * @author Looly + * @since 4.1.15 + */ +public class SyncFinisher implements Closeable { + + private final Set workers; + private final int threadSize; + private ExecutorService executorService; + + private boolean isBeginAtSameTime; + /** + * 启动同步器,用于保证所有worker线程同时开始 + */ + private final CountDownLatch beginLatch; + /** + * 结束同步器,用于等待所有worker线程同时结束 + */ + private CountDownLatch endLatch; + + /** + * 构造 + * + * @param threadSize 线程数 + */ + public SyncFinisher(int threadSize) { + this.beginLatch = new CountDownLatch(1); + this.threadSize = threadSize; + this.workers = new LinkedHashSet<>(); + } + + /** + * 设置是否所有worker线程同时开始 + * + * @param isBeginAtSameTime 是否所有worker线程同时开始 + * @return this + */ + public SyncFinisher setBeginAtSameTime(boolean isBeginAtSameTime) { + this.isBeginAtSameTime = isBeginAtSameTime; + return this; + } + + /** + * 增加定义的线程数同等数量的worker + * + * @param runnable 工作线程 + * @return this + */ + public SyncFinisher addRepeatWorker(final Runnable runnable) { + for (int i = 0; i < this.threadSize; i++) { + addWorker(new Worker() { + @Override + public void work() { + runnable.run(); + } + }); + } + return this; + } + + /** + * 增加工作线程 + * + * @param runnable 工作线程 + * @return this + */ + public SyncFinisher addWorker(final Runnable runnable) { + return addWorker(new Worker() { + @Override + public void work() { + runnable.run(); + } + }); + } + + /** + * 增加工作线程 + * + * @param worker 工作线程 + * @return this + */ + synchronized public SyncFinisher addWorker(Worker worker) { + workers.add(worker); + return this; + } + + /** + * 开始工作
+ * 执行此方法后如果不再重复使用此对象,需调用{@link #stop()}关闭回收资源。 + */ + public void start() { + start(true); + } + + /** + * 开始工作
+ * 执行此方法后如果不再重复使用此对象,需调用{@link #stop()}关闭回收资源。 + * + * @param sync 是否阻塞等待 + * @since 4.5.8 + */ + public void start(boolean sync) { + endLatch = new CountDownLatch(workers.size()); + + if (null == this.executorService || this.executorService.isShutdown()) { + this.executorService = ThreadUtil.newExecutor(threadSize); + } + for (Worker worker : workers) { + executorService.submit(worker); + } + // 保证所有worker同时开始 + this.beginLatch.countDown(); + + if (sync) { + try { + this.endLatch.await(); + } catch (InterruptedException e) { + throw new UtilException(e); + } + } + } + + /** + * 结束线程池。此方法执行两种情况: + *
    + *
  1. 执行start(true)后,调用此方法结束线程池回收资源
  2. + *
  3. 执行start(false)后,用户自行判断结束点执行此方法
  4. + *
+ * + * @since 5.6.6 + */ + public void stop() { + if (null != this.executorService) { + this.executorService.shutdown(); + this.executorService = null; + } + + clearWorker(); + } + + /** + * 立即结束线程池所有线程。此方法执行两种情况: + *
    + *
  1. 执行start(true)后,调用此方法结束线程池回收资源
  2. + *
  3. 执行start(false)后,用户自行判断结束点执行此方法
  4. + *
+ * + * @since 5.8.11 + */ + public void stopNow() { + if (null != this.executorService) { + this.executorService.shutdownNow(); + this.executorService = null; + } + + clearWorker(); + } + + /** + * 清空工作线程对象 + */ + public void clearWorker() { + workers.clear(); + } + + /** + * 剩余任务数 + * + * @return 剩余任务数 + */ + public long count() { + return endLatch.getCount(); + } + + @Override + public void close() throws IOException { + stop(); + } + + /** + * 工作者,为一个线程 + * + * @author xiaoleilu + */ + public abstract class Worker implements Runnable { + + @Override + public void run() { + if (isBeginAtSameTime) { + try { + beginLatch.await(); + } catch (InterruptedException e) { + throw new UtilException(e); + } + } + try { + work(); + } finally { + endLatch.countDown(); + } + } + + /** + * 任务内容 + */ + public abstract void work(); + } +} diff --git a/src/main/java/cn/hutool/core/thread/ThreadException.java b/src/main/java/cn/hutool/core/thread/ThreadException.java new file mode 100644 index 0000000..b61963b --- /dev/null +++ b/src/main/java/cn/hutool/core/thread/ThreadException.java @@ -0,0 +1,38 @@ +package cn.hutool.core.thread; + +import cn.hutool.core.exceptions.ExceptionUtil; +import cn.hutool.core.util.StrUtil; + +/** + * 工具类异常 + * + * @author looly + * @since 5.7.17 + */ +public class ThreadException extends RuntimeException { + private static final long serialVersionUID = 5253124428623713216L; + + public ThreadException(Throwable e) { + super(ExceptionUtil.getMessage(e), e); + } + + public ThreadException(String message) { + super(message); + } + + public ThreadException(String messageTemplate, Object... params) { + super(StrUtil.format(messageTemplate, params)); + } + + public ThreadException(String message, Throwable throwable) { + super(message, throwable); + } + + public ThreadException(String message, Throwable throwable, boolean enableSuppression, boolean writableStackTrace) { + super(message, throwable, enableSuppression, writableStackTrace); + } + + public ThreadException(Throwable throwable, String messageTemplate, Object... params) { + super(StrUtil.format(messageTemplate, params), throwable); + } +} diff --git a/src/main/java/cn/hutool/core/thread/ThreadFactoryBuilder.java b/src/main/java/cn/hutool/core/thread/ThreadFactoryBuilder.java new file mode 100644 index 0000000..686f95c --- /dev/null +++ b/src/main/java/cn/hutool/core/thread/ThreadFactoryBuilder.java @@ -0,0 +1,157 @@ +package cn.hutool.core.thread; + +import cn.hutool.core.builder.Builder; +import cn.hutool.core.util.StrUtil; + +import java.lang.Thread.UncaughtExceptionHandler; +import java.util.concurrent.Executors; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.atomic.AtomicLong; + +/** + * ThreadFactory创建器
+ * 参考:Guava的ThreadFactoryBuilder + * + * @author looly + * @since 4.1.9 + */ +public class ThreadFactoryBuilder implements Builder { + private static final long serialVersionUID = 1L; + + /** + * 用于线程创建的线程工厂类 + */ + private ThreadFactory backingThreadFactory; + /** + * 线程名的前缀 + */ + private String namePrefix; + /** + * 是否守护线程,默认false + */ + private Boolean daemon; + /** + * 线程优先级 + */ + private Integer priority; + /** + * 未捕获异常处理器 + */ + private UncaughtExceptionHandler uncaughtExceptionHandler; + + /** + * 创建{@code ThreadFactoryBuilder} + * + * @return {@code ThreadFactoryBuilder} + */ + public static ThreadFactoryBuilder create() { + return new ThreadFactoryBuilder(); + } + + /** + * 设置用于创建基础线程的线程工厂 + * + * @param backingThreadFactory 用于创建基础线程的线程工厂 + * @return this + */ + public ThreadFactoryBuilder setThreadFactory(ThreadFactory backingThreadFactory) { + this.backingThreadFactory = backingThreadFactory; + return this; + } + + /** + * 设置线程名前缀,例如设置前缀为hutool-thread-,则线程名为hutool-thread-1之类。 + * + * @param namePrefix 线程名前缀 + * @return this + */ + public ThreadFactoryBuilder setNamePrefix(String namePrefix) { + this.namePrefix = namePrefix; + return this; + } + + /** + * 设置是否守护线程 + * + * @param daemon 是否守护线程 + * @return this + */ + public ThreadFactoryBuilder setDaemon(boolean daemon) { + this.daemon = daemon; + return this; + } + + /** + * 设置线程优先级 + * + * @param priority 优先级 + * @return this + * @see Thread#MIN_PRIORITY + * @see Thread#NORM_PRIORITY + * @see Thread#MAX_PRIORITY + */ + public ThreadFactoryBuilder setPriority(int priority) { + if (priority < Thread.MIN_PRIORITY) { + throw new IllegalArgumentException(StrUtil.format("Thread priority ({}) must be >= {}", priority, Thread.MIN_PRIORITY)); + } + if (priority > Thread.MAX_PRIORITY) { + throw new IllegalArgumentException(StrUtil.format("Thread priority ({}) must be <= {}", priority, Thread.MAX_PRIORITY)); + } + this.priority = priority; + return this; + } + + /** + * 设置未捕获异常的处理方式 + * + * @param uncaughtExceptionHandler {@link UncaughtExceptionHandler} + * @return this + */ + public ThreadFactoryBuilder setUncaughtExceptionHandler(UncaughtExceptionHandler uncaughtExceptionHandler) { + this.uncaughtExceptionHandler = uncaughtExceptionHandler; + return this; + } + + /** + * 构建{@link ThreadFactory} + * + * @return {@link ThreadFactory} + */ + @Override + public ThreadFactory build() { + return build(this); + } + + /** + * 构建 + * + * @param builder {@code ThreadFactoryBuilder} + * @return {@link ThreadFactory} + */ + private static ThreadFactory build(ThreadFactoryBuilder builder) { + final ThreadFactory backingThreadFactory = (null != builder.backingThreadFactory)// + ? builder.backingThreadFactory // + : Executors.defaultThreadFactory(); + final String namePrefix = builder.namePrefix; + final Boolean daemon = builder.daemon; + final Integer priority = builder.priority; + final UncaughtExceptionHandler handler = builder.uncaughtExceptionHandler; + final AtomicLong count = (null == namePrefix) ? null : new AtomicLong(); + return r -> { + final Thread thread = backingThreadFactory.newThread(r); + if (null != namePrefix) { + thread.setName(namePrefix + count.getAndIncrement()); + } + if (null != daemon) { + thread.setDaemon(daemon); + } + if (null != priority) { + thread.setPriority(priority); + } + if (null != handler) { + thread.setUncaughtExceptionHandler(handler); + } + return thread; + }; + } +} diff --git a/src/main/java/cn/hutool/core/thread/ThreadUtil.java b/src/main/java/cn/hutool/core/thread/ThreadUtil.java new file mode 100644 index 0000000..55b9461 --- /dev/null +++ b/src/main/java/cn/hutool/core/thread/ThreadUtil.java @@ -0,0 +1,676 @@ +package cn.hutool.core.thread; + +import java.lang.Thread.UncaughtExceptionHandler; +import java.util.concurrent.*; +import java.util.function.Supplier; + +/** + * 线程池工具 + * + * @author luxiaolei + */ +public class ThreadUtil { + + /** + * 新建一个线程池,默认的策略如下: + *
+	 *    1. 初始线程数为corePoolSize指定的大小
+	 *    2. 没有最大线程数限制
+	 *    3. 默认使用LinkedBlockingQueue,默认队列大小为1024
+	 * 
+ * + * @param corePoolSize 同时执行的线程数大小 + * @return ExecutorService + */ + public static ExecutorService newExecutor(int corePoolSize) { + ExecutorBuilder builder = ExecutorBuilder.create(); + if (corePoolSize > 0) { + builder.setCorePoolSize(corePoolSize); + } + return builder.build(); + } + + /** + * 获得一个新的线程池,默认的策略如下: + *
+	 *    1. 初始线程数为 0
+	 *    2. 最大线程数为Integer.MAX_VALUE
+	 *    3. 使用SynchronousQueue
+	 *    4. 任务直接提交给线程而不保持它们
+	 * 
+ * + * @return ExecutorService + */ + public static ExecutorService newExecutor() { + return ExecutorBuilder.create().useSynchronousQueue().build(); + } + + /** + * 获得一个新的线程池,只有单个线程,策略如下: + *
+	 *    1. 初始线程数为 1
+	 *    2. 最大线程数为 1
+	 *    3. 默认使用LinkedBlockingQueue,默认队列大小为1024
+	 *    4. 同时只允许一个线程工作,剩余放入队列等待,等待数超过1024报错
+	 * 
+ * + * @return ExecutorService + */ + public static ExecutorService newSingleExecutor() { + return ExecutorBuilder.create()// + .setCorePoolSize(1)// + .setMaxPoolSize(1)// + .setKeepAliveTime(0)// + .buildFinalizable(); + } + + /** + * 获得一个新的线程池
+ * 如果maximumPoolSize >= corePoolSize,在没有新任务加入的情况下,多出的线程将最多保留60s + * + * @param corePoolSize 初始线程池大小 + * @param maximumPoolSize 最大线程池大小 + * @return {@link ThreadPoolExecutor} + */ + public static ThreadPoolExecutor newExecutor(int corePoolSize, int maximumPoolSize) { + return ExecutorBuilder.create() + .setCorePoolSize(corePoolSize) + .setMaxPoolSize(maximumPoolSize) + .build(); + } + + /** + * 获得一个新的线程池,并指定最大任务队列大小
+ * 如果maximumPoolSize >= corePoolSize,在没有新任务加入的情况下,多出的线程将最多保留60s + * + * @param corePoolSize 初始线程池大小 + * @param maximumPoolSize 最大线程池大小 + * @param maximumQueueSize 最大任务队列大小 + * @return {@link ThreadPoolExecutor} + * @since 5.4.1 + */ + public static ExecutorService newExecutor(int corePoolSize, int maximumPoolSize, int maximumQueueSize) { + return ExecutorBuilder.create() + .setCorePoolSize(corePoolSize) + .setMaxPoolSize(maximumPoolSize) + .setWorkQueue(new LinkedBlockingQueue<>(maximumQueueSize)) + .build(); + } + + /** + * 获取一个新的线程池,默认的策略如下
+ *
+	 *     1. 核心线程数与最大线程数为nThreads指定的大小
+	 *     2. 默认使用LinkedBlockingQueue,默认队列大小为1024
+	 *     3. 如果isBlocked为{code true},当执行拒绝策略的时候会处于阻塞状态,直到能添加到队列中或者被{@link Thread#interrupt()}中断
+	 * 
+ * + * @param nThreads 线程池大小 + * @param threadNamePrefix 线程名称前缀 + * @param isBlocked 是否使用{@link BlockPolicy}策略 + * @return ExecutorService + * @author luozongle + * @since 5.8.0 + */ + public static ExecutorService newFixedExecutor(int nThreads, String threadNamePrefix, boolean isBlocked) { + return newFixedExecutor(nThreads, 1024, threadNamePrefix, isBlocked); + } + + /** + * 获取一个新的线程池,默认的策略如下
+ *
+	 *     1. 核心线程数与最大线程数为nThreads指定的大小
+	 *     2. 默认使用LinkedBlockingQueue
+	 *     3. 如果isBlocked为{code true},当执行拒绝策略的时候会处于阻塞状态,直到能添加到队列中或者被{@link Thread#interrupt()}中断
+	 * 
+ * + * @param nThreads 线程池大小 + * @param maximumQueueSize 队列大小 + * @param threadNamePrefix 线程名称前缀 + * @param isBlocked 是否使用{@link BlockPolicy}策略 + * @return ExecutorService + * @author luozongle + * @since 5.8.0 + */ + public static ExecutorService newFixedExecutor(int nThreads, int maximumQueueSize, String threadNamePrefix, boolean isBlocked) { + return newFixedExecutor(nThreads, maximumQueueSize, threadNamePrefix, + (isBlocked ? RejectPolicy.BLOCK : RejectPolicy.ABORT).getValue()); + } + + /** + * 获得一个新的线程池,默认策略如下
+ *
+	 *     1. 核心线程数与最大线程数为nThreads指定的大小
+	 *     2. 默认使用LinkedBlockingQueue
+	 * 
+ * + * @param nThreads 线程池大小 + * @param maximumQueueSize 队列大小 + * @param threadNamePrefix 线程名称前缀 + * @param handler 拒绝策略 + * @return ExecutorService + * @author luozongle + * @since 5.8.0 + */ + public static ExecutorService newFixedExecutor(int nThreads, + int maximumQueueSize, + String threadNamePrefix, + RejectedExecutionHandler handler) { + return ExecutorBuilder.create() + .setCorePoolSize(nThreads).setMaxPoolSize(nThreads) + .setWorkQueue(new LinkedBlockingQueue<>(maximumQueueSize)) + .setThreadFactory(createThreadFactory(threadNamePrefix)) + .setHandler(handler) + .build(); + } + + /** + * 直接在公共线程池中执行线程 + * + * @param runnable 可运行对象 + */ + public static void execute(Runnable runnable) { + GlobalThreadPool.execute(runnable); + } + + /** + * 执行异步方法 + * + * @param runnable 需要执行的方法体 + * @param isDaemon 是否守护线程。守护线程会在主线程结束后自动结束 + * @return 执行的方法体 + */ + public static Runnable execAsync(Runnable runnable, boolean isDaemon) { + Thread thread = new Thread(runnable); + thread.setDaemon(isDaemon); + thread.start(); + + return runnable; + } + + /** + * 执行有返回值的异步方法
+ * Future代表一个异步执行的操作,通过get()方法可以获得操作的结果,如果异步操作还没有完成,则,get()会使当前线程阻塞 + * + * @param 回调对象类型 + * @param task {@link Callable} + * @return Future + */ + public static Future execAsync(Callable task) { + return GlobalThreadPool.submit(task); + } + + /** + * 执行有返回值的异步方法
+ * Future代表一个异步执行的操作,通过get()方法可以获得操作的结果,如果异步操作还没有完成,则,get()会使当前线程阻塞 + * + * @param runnable 可运行对象 + * @return {@link Future} + * @since 3.0.5 + */ + public static Future execAsync(Runnable runnable) { + return GlobalThreadPool.submit(runnable); + } + + /** + * 新建一个CompletionService,调用其submit方法可以异步执行多个任务,最后调用take方法按照完成的顺序获得其结果。
+ * 若未完成,则会阻塞 + * + * @param 回调对象类型 + * @return CompletionService + */ + public static CompletionService newCompletionService() { + return new ExecutorCompletionService<>(GlobalThreadPool.getExecutor()); + } + + /** + * 新建一个CompletionService,调用其submit方法可以异步执行多个任务,最后调用take方法按照完成的顺序获得其结果。
+ * 若未完成,则会阻塞 + * + * @param 回调对象类型 + * @param executor 执行器 {@link ExecutorService} + * @return CompletionService + */ + public static CompletionService newCompletionService(ExecutorService executor) { + return new ExecutorCompletionService<>(executor); + } + + /** + * 新建一个CountDownLatch,一个同步辅助类,在完成一组正在其他线程中执行的操作之前,它允许一个或多个线程一直等待。 + * + * @param threadCount 线程数量 + * @return CountDownLatch + */ + public static CountDownLatch newCountDownLatch(int threadCount) { + return new CountDownLatch(threadCount); + } + + /** + * 创建新线程,非守护线程,正常优先级,线程组与当前线程的线程组一致 + * + * @param runnable {@link Runnable} + * @param name 线程名 + * @return {@link Thread} + * @since 3.1.2 + */ + public static Thread newThread(Runnable runnable, String name) { + final Thread t = newThread(runnable, name, false); + if (t.getPriority() != Thread.NORM_PRIORITY) { + t.setPriority(Thread.NORM_PRIORITY); + } + return t; + } + + /** + * 创建新线程 + * + * @param runnable {@link Runnable} + * @param name 线程名 + * @param isDaemon 是否守护线程 + * @return {@link Thread} + * @since 4.1.2 + */ + public static Thread newThread(Runnable runnable, String name, boolean isDaemon) { + final Thread t = new Thread(null, runnable, name); + t.setDaemon(isDaemon); + return t; + } + + /** + * 挂起当前线程 + * + * @param timeout 挂起的时长 + * @param timeUnit 时长单位 + * @return 被中断返回false,否则true + */ + public static boolean sleep(Number timeout, TimeUnit timeUnit) { + try { + timeUnit.sleep(timeout.longValue()); + } catch (InterruptedException e) { + return false; + } + return true; + } + + /** + * 挂起当前线程 + * + * @param millis 挂起的毫秒数 + * @return 被中断返回false,否则true + */ + public static boolean sleep(Number millis) { + if (millis == null) { + return true; + } + return sleep(millis.longValue()); + } + + /** + * 挂起当前线程 + * + * @param millis 挂起的毫秒数 + * @return 被中断返回false,否则true + * @since 5.3.2 + */ + public static boolean sleep(long millis) { + if (millis > 0) { + try { + Thread.sleep(millis); + } catch (InterruptedException e) { + return false; + } + } + return true; + } + + /** + * 考虑{@link Thread#sleep(long)}方法有可能时间不足给定毫秒数,此方法保证sleep时间不小于给定的毫秒数 + * + * @param millis 给定的sleep时间 + * @return 被中断返回false,否则true + * @see ThreadUtil#sleep(Number) + */ + public static boolean safeSleep(Number millis) { + if (millis == null) { + return true; + } + + return safeSleep(millis.longValue()); + } + + /** + * 考虑{@link Thread#sleep(long)}方法有可能时间不足给定毫秒数,此方法保证sleep时间不小于给定的毫秒数 + * + * @param millis 给定的sleep时间 + * @return 被中断返回false,否则true + * @see ThreadUtil#sleep(Number) + * @since 5.3.2 + */ + public static boolean safeSleep(long millis) { + long done = 0; + long before; + long spendTime; + while (done >= 0 && done < millis) { + before = System.currentTimeMillis(); + if (!sleep(millis - done)) { + return false; + } + spendTime = System.currentTimeMillis() - before; + if (spendTime <= 0) { + // Sleep花费时间为0或者负数,说明系统时间被拨动 + break; + } + done += spendTime; + } + return true; + } + + /** + * @return 获得堆栈列表 + */ + public static StackTraceElement[] getStackTrace() { + return Thread.currentThread().getStackTrace(); + } + + /** + * 获得堆栈项 + * + * @param i 第几个堆栈项 + * @return 堆栈项 + */ + public static StackTraceElement getStackTraceElement(int i) { + StackTraceElement[] stackTrace = getStackTrace(); + if (i < 0) { + i += stackTrace.length; + } + return stackTrace[i]; + } + + /** + * 创建本地线程对象 + * + * @param 持有对象类型 + * @param isInheritable 是否为子线程提供从父线程那里继承的值 + * @return 本地线程 + */ + public static ThreadLocal createThreadLocal(boolean isInheritable) { + if (isInheritable) { + return new InheritableThreadLocal<>(); + } else { + return new ThreadLocal<>(); + } + } + + /** + * 创建本地线程对象 + * + * @param 持有对象类型 + * @param supplier 初始化线程对象函数 + * @return 本地线程 + * @see ThreadLocal#withInitial(Supplier) + * @since 5.6.7 + */ + public static ThreadLocal createThreadLocal(Supplier supplier) { + return ThreadLocal.withInitial(supplier); + } + + /** + * 创建ThreadFactoryBuilder + * + * @return ThreadFactoryBuilder + * @see ThreadFactoryBuilder#build() + * @since 4.1.13 + */ + public static ThreadFactoryBuilder createThreadFactoryBuilder() { + return ThreadFactoryBuilder.create(); + } + + /** + * 创建自定义线程名称前缀的{@link ThreadFactory} + * + * @param threadNamePrefix 线程名称前缀 + * @return {@link ThreadFactory} + * @see ThreadFactoryBuilder#build() + * @since 5.8.0 + */ + public static ThreadFactory createThreadFactory(String threadNamePrefix) { + return ThreadFactoryBuilder.create().setNamePrefix(threadNamePrefix).build(); + } + + /** + * 结束线程,调用此方法后,线程将抛出 {@link InterruptedException}异常 + * + * @param thread 线程 + * @param isJoin 是否等待结束 + */ + public static void interrupt(Thread thread, boolean isJoin) { + if (null != thread && !thread.isInterrupted()) { + thread.interrupt(); + if (isJoin) { + waitForDie(thread); + } + } + } + + /** + * 等待当前线程结束. 调用 {@link Thread#join()} 并忽略 {@link InterruptedException} + */ + public static void waitForDie() { + waitForDie(Thread.currentThread()); + } + + /** + * 等待线程结束. 调用 {@link Thread#join()} 并忽略 {@link InterruptedException} + * + * @param thread 线程 + */ + public static void waitForDie(Thread thread) { + if (null == thread) { + return; + } + + boolean dead = false; + do { + try { + thread.join(); + dead = true; + } catch (InterruptedException e) { + // ignore + } + } while (!dead); + } + + /** + * 获取JVM中与当前线程同组的所有线程
+ * + * @return 线程对象数组 + */ + public static Thread[] getThreads() { + return getThreads(Thread.currentThread().getThreadGroup().getParent()); + } + + /** + * 获取JVM中与当前线程同组的所有线程
+ * 使用数组二次拷贝方式,防止在线程列表获取过程中线程终止
+ * from Voovan + * + * @param group 线程组 + * @return 线程对象数组 + */ + public static Thread[] getThreads(ThreadGroup group) { + final Thread[] slackList = new Thread[group.activeCount() * 2]; + final int actualSize = group.enumerate(slackList); + final Thread[] result = new Thread[actualSize]; + System.arraycopy(slackList, 0, result, 0, actualSize); + return result; + } + + /** + * 获取进程的主线程
+ * from Voovan + * + * @return 进程的主线程 + */ + public static Thread getMainThread() { + for (Thread thread : getThreads()) { + if (thread.getId() == 1) { + return thread; + } + } + return null; + } + + /** + * 获取当前线程的线程组 + * + * @return 线程组 + * @since 3.1.2 + */ + public static ThreadGroup currentThreadGroup() { + final SecurityManager s = System.getSecurityManager(); + return (null != s) ? s.getThreadGroup() : Thread.currentThread().getThreadGroup(); + } + + /** + * 创建线程工厂 + * + * @param prefix 线程名前缀 + * @param isDaemon 是否守护线程 + * @return {@link ThreadFactory} + * @since 4.0.0 + */ + public static ThreadFactory newNamedThreadFactory(String prefix, boolean isDaemon) { + return new NamedThreadFactory(prefix, isDaemon); + } + + /** + * 创建线程工厂 + * + * @param prefix 线程名前缀 + * @param threadGroup 线程组,可以为null + * @param isDaemon 是否守护线程 + * @return {@link ThreadFactory} + * @since 4.0.0 + */ + public static ThreadFactory newNamedThreadFactory(String prefix, ThreadGroup threadGroup, boolean isDaemon) { + return new NamedThreadFactory(prefix, threadGroup, isDaemon); + } + + /** + * 创建线程工厂 + * + * @param prefix 线程名前缀 + * @param threadGroup 线程组,可以为null + * @param isDaemon 是否守护线程 + * @param handler 未捕获异常处理 + * @return {@link ThreadFactory} + * @since 4.0.0 + */ + public static ThreadFactory newNamedThreadFactory(String prefix, ThreadGroup threadGroup, boolean isDaemon, UncaughtExceptionHandler handler) { + return new NamedThreadFactory(prefix, threadGroup, isDaemon, handler); + } + + /** + * 阻塞当前线程,保证在main方法中执行不被退出 + * + * @param obj 对象所在线程 + * @since 4.5.6 + */ + @SuppressWarnings("SynchronizationOnLocalVariableOrMethodParameter") + public static void sync(Object obj) { + synchronized (obj) { + try { + obj.wait(); + } catch (InterruptedException e) { + // ignore + } + } + } + + /** + * 并发测试
+ * 此方法用于测试多线程下执行某些逻辑的并发性能
+ * 调用此方法会导致当前线程阻塞。
+ * 结束后可调用{@link ConcurrencyTester#getInterval()} 方法获取执行时间 + * + * @param threadSize 并发线程数 + * @param runnable 执行的逻辑实现 + * @return {@link ConcurrencyTester} + * @since 4.5.8 + */ + @SuppressWarnings("resource") + public static ConcurrencyTester concurrencyTest(int threadSize, Runnable runnable) { + return (new ConcurrencyTester(threadSize)).test(runnable); + } + + /** + * 创建{@link ScheduledThreadPoolExecutor} + * + * @param corePoolSize 初始线程池大小 + * @return {@link ScheduledThreadPoolExecutor} + * @since 5.5.8 + */ + public static ScheduledThreadPoolExecutor createScheduledExecutor(int corePoolSize) { + return new ScheduledThreadPoolExecutor(corePoolSize); + } + + /** + * 开始执行一个定时任务,执行方式分fixedRate模式和fixedDelay模式。
+ * 注意:此方法的延迟和周期的单位均为毫秒。 + * + *
    + *
  • fixedRate 模式:以固定的频率执行。每period的时刻检查,如果上个任务完成,启动下个任务,否则等待上个任务结束后立即启动。
  • + *
  • fixedDelay模式:以固定的延时执行。上次任务结束后等待period再执行下个任务。
  • + *
+ * + * @param executor 定时任务线程池,{@code null}新建一个默认线程池 + * @param command 需要定时执行的逻辑 + * @param initialDelay 初始延迟,单位毫秒 + * @param period 执行周期,单位毫秒 + * @param fixedRateOrFixedDelay {@code true}表示fixedRate模式,{@code false}表示fixedDelay模式 + * @return {@link ScheduledThreadPoolExecutor} + * @since 5.5.8 + */ + public static ScheduledThreadPoolExecutor schedule(ScheduledThreadPoolExecutor executor, + Runnable command, + long initialDelay, + long period, + boolean fixedRateOrFixedDelay) { + return schedule(executor, command, initialDelay, period, TimeUnit.MILLISECONDS, fixedRateOrFixedDelay); + } + + /** + * 开始执行一个定时任务,执行方式分fixedRate模式和fixedDelay模式。 + * + *
    + *
  • fixedRate 模式:以固定的频率执行。每period的时刻检查,如果上个任务完成,启动下个任务,否则等待上个任务结束后立即启动。
  • + *
  • fixedDelay模式:以固定的延时执行。上次任务结束后等待period再执行下个任务。
  • + *
+ * + * @param executor 定时任务线程池,{@code null}新建一个默认线程池 + * @param command 需要定时执行的逻辑 + * @param initialDelay 初始延迟 + * @param period 执行周期 + * @param timeUnit 时间单位 + * @param fixedRateOrFixedDelay {@code true}表示fixedRate模式,{@code false}表示fixedDelay模式 + * @return {@link ScheduledThreadPoolExecutor} + * @since 5.6.5 + */ + public static ScheduledThreadPoolExecutor schedule(ScheduledThreadPoolExecutor executor, + Runnable command, + long initialDelay, + long period, + TimeUnit timeUnit, + boolean fixedRateOrFixedDelay) { + if (null == executor) { + executor = createScheduledExecutor(2); + } + if (fixedRateOrFixedDelay) { + executor.scheduleAtFixedRate(command, initialDelay, period, timeUnit); + } else { + executor.scheduleWithFixedDelay(command, initialDelay, period, timeUnit); + } + + return executor; + } +} diff --git a/src/main/java/cn/hutool/core/thread/lock/LockUtil.java b/src/main/java/cn/hutool/core/thread/lock/LockUtil.java new file mode 100644 index 0000000..8a0d12b --- /dev/null +++ b/src/main/java/cn/hutool/core/thread/lock/LockUtil.java @@ -0,0 +1,43 @@ +package cn.hutool.core.thread.lock; + +import java.util.concurrent.locks.ReentrantReadWriteLock; +import java.util.concurrent.locks.StampedLock; + +/** + * 锁相关工具 + * + * @author looly + * @since 5.2.5 + */ +public class LockUtil { + + private static final NoLock NO_LOCK = new NoLock(); + + /** + * 创建{@link StampedLock}锁 + * + * @return {@link StampedLock}锁 + */ + public static StampedLock createStampLock() { + return new StampedLock(); + } + + /** + * 创建{@link ReentrantReadWriteLock}锁 + * + * @param fair 是否公平锁 + * @return {@link ReentrantReadWriteLock}锁 + */ + public static ReentrantReadWriteLock createReadWriteLock(boolean fair) { + return new ReentrantReadWriteLock(fair); + } + + /** + * 获取单例的无锁对象 + * + * @return {@link NoLock} + */ + public static NoLock getNoLock(){ + return NO_LOCK; + } +} diff --git a/src/main/java/cn/hutool/core/thread/lock/NoLock.java b/src/main/java/cn/hutool/core/thread/lock/NoLock.java new file mode 100644 index 0000000..09d6b86 --- /dev/null +++ b/src/main/java/cn/hutool/core/thread/lock/NoLock.java @@ -0,0 +1,46 @@ +package cn.hutool.core.thread.lock; + +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.Condition; +import java.util.concurrent.locks.Lock; + +/** + * 无锁实现 + * + * @author looly + *@since 4.3.1 + */ +public class NoLock implements Lock{ + + public static NoLock INSTANCE = new NoLock(); + + @Override + public void lock() { + } + + @Override + public void lockInterruptibly() { + } + + @Override + public boolean tryLock() { + return true; + } + + @SuppressWarnings("NullableProblems") + @Override + public boolean tryLock(long time, TimeUnit unit) { + return true; + } + + @Override + public void unlock() { + } + + @SuppressWarnings("NullableProblems") + @Override + public Condition newCondition() { + throw new UnsupportedOperationException("NoLock`s newCondition method is unsupported"); + } + +} diff --git a/src/main/java/cn/hutool/core/thread/lock/NoReadWriteLock.java b/src/main/java/cn/hutool/core/thread/lock/NoReadWriteLock.java new file mode 100644 index 0000000..ce84885 --- /dev/null +++ b/src/main/java/cn/hutool/core/thread/lock/NoReadWriteLock.java @@ -0,0 +1,22 @@ +package cn.hutool.core.thread.lock; + +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReadWriteLock; + +/** + * 无锁的读写锁实现 + * + * @author looly + * @since 5.8.0 + */ +public class NoReadWriteLock implements ReadWriteLock { + @Override + public Lock readLock() { + return NoLock.INSTANCE; + } + + @Override + public Lock writeLock() { + return NoLock.INSTANCE; + } +} diff --git a/src/main/java/cn/hutool/core/thread/lock/package-info.java b/src/main/java/cn/hutool/core/thread/lock/package-info.java new file mode 100644 index 0000000..d442ffb --- /dev/null +++ b/src/main/java/cn/hutool/core/thread/lock/package-info.java @@ -0,0 +1,7 @@ +/** + * 锁的实现 + * + * @author looly + * + */ +package cn.hutool.core.thread.lock; \ No newline at end of file diff --git a/src/main/java/cn/hutool/core/thread/package-info.java b/src/main/java/cn/hutool/core/thread/package-info.java new file mode 100644 index 0000000..447309c --- /dev/null +++ b/src/main/java/cn/hutool/core/thread/package-info.java @@ -0,0 +1,7 @@ +/** + * 提供线程及高并发封装,入口为ThreadUtil + * + * @author looly + * + */ +package cn.hutool.core.thread; \ No newline at end of file diff --git a/src/main/java/cn/hutool/core/thread/threadlocal/NamedInheritableThreadLocal.java b/src/main/java/cn/hutool/core/thread/threadlocal/NamedInheritableThreadLocal.java new file mode 100644 index 0000000..943aa7d --- /dev/null +++ b/src/main/java/cn/hutool/core/thread/threadlocal/NamedInheritableThreadLocal.java @@ -0,0 +1,28 @@ +package cn.hutool.core.thread.threadlocal; + +/** + * 带有Name标识的 {@link InheritableThreadLocal},调用toString返回name + * + * @param 值类型 + * @author looly + * @since 4.1.4 + */ +public class NamedInheritableThreadLocal extends InheritableThreadLocal { + + private final String name; + + /** + * 构造 + * + * @param name 名字 + */ + public NamedInheritableThreadLocal(String name) { + this.name = name; + } + + @Override + public String toString() { + return this.name; + } + +} diff --git a/src/main/java/cn/hutool/core/thread/threadlocal/NamedThreadLocal.java b/src/main/java/cn/hutool/core/thread/threadlocal/NamedThreadLocal.java new file mode 100644 index 0000000..babe879 --- /dev/null +++ b/src/main/java/cn/hutool/core/thread/threadlocal/NamedThreadLocal.java @@ -0,0 +1,28 @@ +package cn.hutool.core.thread.threadlocal; + +/** + * 带有Name标识的 {@link ThreadLocal},调用toString返回name + * + * @param 值类型 + * @author looly + * @since 4.1.4 + */ +public class NamedThreadLocal extends ThreadLocal { + + private final String name; + + /** + * 构造 + * + * @param name 名字 + */ + public NamedThreadLocal(String name) { + this.name = name; + } + + @Override + public String toString() { + return this.name; + } + +} diff --git a/src/main/java/cn/hutool/core/thread/threadlocal/package-info.java b/src/main/java/cn/hutool/core/thread/threadlocal/package-info.java new file mode 100644 index 0000000..5ca1a0f --- /dev/null +++ b/src/main/java/cn/hutool/core/thread/threadlocal/package-info.java @@ -0,0 +1,7 @@ +/** + * + * ThreadLocal相关封装 + * @author looly + * + */ +package cn.hutool.core.thread.threadlocal; \ No newline at end of file diff --git a/src/main/java/cn/hutool/core/util/ArrayUtil.java b/src/main/java/cn/hutool/core/util/ArrayUtil.java new file mode 100644 index 0000000..21d13eb --- /dev/null +++ b/src/main/java/cn/hutool/core/util/ArrayUtil.java @@ -0,0 +1,1975 @@ +package cn.hutool.core.util; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.collection.UniqueKeySet; +import cn.hutool.core.comparator.CompareUtil; +import cn.hutool.core.convert.Convert; +import cn.hutool.core.exceptions.UtilException; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.lang.Editor; +import cn.hutool.core.lang.Filter; +import cn.hutool.core.lang.Matcher; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.text.StrJoiner; + +import java.lang.reflect.Array; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Random; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * 数组工具类 + * + * @author Looly + */ +public class ArrayUtil extends PrimitiveArrayUtil { + + // ---------------------------------------------------------------------- isEmpty + + /** + * 数组是否为空 + * + * @param 数组元素类型 + * @param array 数组 + * @return 是否为空 + */ + public static boolean isEmpty(T[] array) { + return array == null || array.length == 0; + } + + /** + * 如果给定数组为空,返回默认数组 + * + * @param 数组元素类型 + * @param array 数组 + * @param defaultArray 默认数组 + * @return 非空(empty)的原数组或默认数组 + * @since 4.6.9 + */ + public static T[] defaultIfEmpty(T[] array, T[] defaultArray) { + return isEmpty(array) ? defaultArray : array; + } + + /** + * 数组是否为空
+ * 此方法会匹配单一对象,如果此对象为{@code null}则返回true
+ * 如果此对象为非数组,理解为此对象为数组的第一个元素,则返回false
+ * 如果此对象为数组对象,数组长度大于0情况下返回false,否则返回true + * + * @param array 数组 + * @return 是否为空 + */ + public static boolean isEmpty(Object array) { + if (array != null) { + if (isArray(array)) { + return 0 == Array.getLength(array); + } + return false; + } + return true; + } + + // ---------------------------------------------------------------------- isNotEmpty + + /** + * 数组是否为非空 + * + * @param 数组元素类型 + * @param array 数组 + * @return 是否为非空 + */ + public static boolean isNotEmpty(T[] array) { + return (null != array && array.length != 0); + } + + /** + * 数组是否为非空
+ * 此方法会匹配单一对象,如果此对象为{@code null}则返回false
+ * 如果此对象为非数组,理解为此对象为数组的第一个元素,则返回true
+ * 如果此对象为数组对象,数组长度大于0情况下返回true,否则返回false + * + * @param array 数组 + * @return 是否为非空 + */ + public static boolean isNotEmpty(Object array) { + return !isEmpty(array); + } + + /** + * 是否包含{@code null}元素 + * + * @param 数组元素类型 + * @param array 被检查的数组 + * @return 是否包含{@code null}元素 + * @since 3.0.7 + */ + @SuppressWarnings("unchecked") + public static boolean hasNull(T... array) { + if (isNotEmpty(array)) { + for (T element : array) { + if (ObjectUtil.isNull(element)) { + return true; + } + } + } + return array == null; + } + + /** + * 多个字段是否全为null + * + * @param 数组元素类型 + * @param array 被检查的数组 + * @return 多个字段是否全为null + * @author dahuoyzs + * @since 5.4.0 + */ + @SuppressWarnings("unchecked") + public static boolean isAllNull(T... array) { + return null == firstNonNull(array); + } + + /** + * 返回数组中第一个非空元素 + * + * @param 数组元素类型 + * @param array 数组 + * @return 非空元素,如果不存在非空元素或数组为空,返回{@code null} + * @since 3.0.7 + */ + @SuppressWarnings("unchecked") + public static T firstNonNull(T... array) { + return firstMatch(ObjectUtil::isNotNull, array); + } + + /** + * 返回数组中第一个匹配规则的值 + * + * @param 数组元素类型 + * @param matcher 匹配接口,实现此接口自定义匹配规则 + * @param array 数组 + * @return 匹配元素,如果不存在匹配元素或数组为空,返回 {@code null} + * @since 3.0.7 + */ + @SuppressWarnings("unchecked") + public static T firstMatch(Matcher matcher, T... array) { + final int index = matchIndex(matcher, array); + if (index < 0) { + return null; + } + + return array[index]; + } + + /** + * 返回数组中第一个匹配规则的值的位置 + * + * @param 数组元素类型 + * @param matcher 匹配接口,实现此接口自定义匹配规则 + * @param array 数组 + * @return 匹配到元素的位置,-1表示未匹配到 + * @since 5.6.6 + */ + @SuppressWarnings("unchecked") + public static int matchIndex(Matcher matcher, T... array) { + return matchIndex(matcher, 0, array); + } + + /** + * 返回数组中第一个匹配规则的值的位置 + * + * @param 数组元素类型 + * @param matcher 匹配接口,实现此接口自定义匹配规则 + * @param beginIndexInclude 检索开始的位置 + * @param array 数组 + * @return 匹配到元素的位置,-1表示未匹配到 + * @since 5.7.3 + */ + @SuppressWarnings("unchecked") + public static int matchIndex(Matcher matcher, int beginIndexInclude, T... array) { + Assert.notNull(matcher, "Matcher must be not null !"); + if (isNotEmpty(array)) { + for (int i = beginIndexInclude; i < array.length; i++) { + if (matcher.match(array[i])) { + return i; + } + } + } + + return INDEX_NOT_FOUND; + } + + /** + * 新建一个空数组 + * + * @param 数组元素类型 + * @param componentType 元素类型 + * @param newSize 大小 + * @return 空数组 + */ + @SuppressWarnings("unchecked") + public static T[] newArray(Class componentType, int newSize) { + return (T[]) Array.newInstance(componentType, newSize); + } + + /** + * 新建一个空数组 + * + * @param newSize 大小 + * @return 空数组 + * @since 3.3.0 + */ + public static Object[] newArray(int newSize) { + return new Object[newSize]; + } + + /** + * 获取数组对象的元素类型 + * + * @param array 数组对象 + * @return 元素类型 + * @since 3.2.2 + */ + public static Class getComponentType(Object array) { + return null == array ? null : array.getClass().getComponentType(); + } + + /** + * 获取数组对象的元素类型 + * + * @param arrayClass 数组类 + * @return 元素类型 + * @since 3.2.2 + */ + public static Class getComponentType(Class arrayClass) { + return null == arrayClass ? null : arrayClass.getComponentType(); + } + + /** + * 根据数组元素类型,获取数组的类型
+ * 方法是通过创建一个空数组从而获取其类型 + * + * @param componentType 数组元素类型 + * @return 数组类型 + * @since 3.2.2 + */ + public static Class getArrayType(Class componentType) { + return Array.newInstance(componentType, 0).getClass(); + } + + /** + * 强转数组类型
+ * 强制转换的前提是数组元素类型可被强制转换
+ * 强制转换后会生成一个新数组 + * + * @param type 数组类型或数组元素类型 + * @param arrayObj 原数组 + * @return 转换后的数组类型 + * @throws NullPointerException 提供参数为空 + * @throws IllegalArgumentException 参数arrayObj不是数组 + * @since 3.0.6 + */ + public static Object[] cast(Class type, Object arrayObj) throws NullPointerException, IllegalArgumentException { + if (null == arrayObj) { + throw new NullPointerException("Argument [arrayObj] is null !"); + } + if (!arrayObj.getClass().isArray()) { + throw new IllegalArgumentException("Argument [arrayObj] is not array !"); + } + if (null == type) { + return (Object[]) arrayObj; + } + + final Class componentType = type.isArray() ? type.getComponentType() : type; + final Object[] array = (Object[]) arrayObj; + final Object[] result = ArrayUtil.newArray(componentType, array.length); + System.arraycopy(array, 0, result, 0, array.length); + return result; + } + + /** + * 将新元素添加到已有数组中
+ * 添加新元素会生成一个新的数组,不影响原数组 + * + * @param 数组元素类型 + * @param buffer 已有数组 + * @param newElements 新元素 + * @return 新数组 + */ + @SafeVarargs + public static T[] append(T[] buffer, T... newElements) { + if (isEmpty(buffer)) { + return newElements; + } + return insert(buffer, buffer.length, newElements); + } + + /** + * 将新元素添加到已有数组中
+ * 添加新元素会生成一个新的数组,不影响原数组 + * + * @param 数组元素类型 + * @param array 已有数组 + * @param newElements 新元素 + * @return 新数组 + */ + @SafeVarargs + public static Object append(Object array, T... newElements) { + if (isEmpty(array)) { + return newElements; + } + return insert(array, length(array), newElements); + } + + /** + * 将元素值设置为数组的某个位置,当给定的index大于数组长度,则追加 + * + * @param 数组元素类型 + * @param buffer 已有数组 + * @param index 位置,大于长度追加,否则替换 + * @param value 新值 + * @return 新数组或原有数组 + * @since 4.1.2 + */ + public static T[] setOrAppend(T[] buffer, int index, T value) { + if (index < buffer.length) { + Array.set(buffer, index, value); + return buffer; + } else { + if(ArrayUtil.isEmpty(buffer)){ + // issue#I5APJE + // 可变长类型在buffer为空的情况下,类型会被擦除,导致报错,此处修正 + final T[] values = newArray(value.getClass(), 1); + values[0] = value; + return append(buffer, values); + } + return append(buffer, value); + } + } + + /** + * 将元素值设置为数组的某个位置,当给定的index大于数组长度,则追加 + * + * @param array 已有数组 + * @param index 位置,大于长度追加,否则替换 + * @param value 新值 + * @return 新数组或原有数组 + * @since 4.1.2 + */ + public static Object setOrAppend(Object array, int index, Object value) { + if (index < length(array)) { + Array.set(array, index, value); + return array; + } else { + return append(array, value); + } + } + + /** + * 将新元素插入到到已有数组中的某个位置
+ * 添加新元素会生成一个新数组或原有数组
+ * 如果插入位置为为负数,那么生成一个由插入元素顺序加已有数组顺序的新数组 + * + * @param 数组元素类型 + * @param buffer 已有数组 + * @param index 位置,大于长度追加,否则替换,<0表示从头部追加 + * @param values 新值 + * @return 新数组或原有数组 + * @since 5.7.23 + */ + @SuppressWarnings({"unchecked"}) + public static T[] replace(T[] buffer, int index, T... values) { + if (isEmpty(values)) { + return buffer; + } + if (isEmpty(buffer)) { + return values; + } + if (index < 0) { + // 从头部追加 + return insert(buffer, 0, values); + } + if (index >= buffer.length) { + // 超出长度,尾部追加 + return append(buffer, values); + } + + if (buffer.length >= values.length + index) { + System.arraycopy(values, 0, buffer, index, values.length); + return buffer; + } + + // 替换长度大于原数组长度,新建数组 + int newArrayLength = index + values.length; + final T[] result = newArray(buffer.getClass().getComponentType(), newArrayLength); + System.arraycopy(buffer, 0, result, 0, index); + System.arraycopy(values, 0, result, index, values.length); + return result; + } + + /** + * 将新元素插入到到已有数组中的某个位置
+ * 添加新元素会生成一个新的数组,不影响原数组
+ * 如果插入位置为为负数,从原数组从后向前计数,若大于原数组长度,则空白处用null填充 + * + * @param 数组元素类型 + * @param buffer 已有数组 + * @param index 插入位置,此位置为对应此位置元素之前的空档 + * @param newElements 新元素 + * @return 新数组 + * @since 4.0.8 + */ + @SuppressWarnings("unchecked") + public static T[] insert(T[] buffer, int index, T... newElements) { + return (T[]) insert((Object) buffer, index, newElements); + } + + /** + * 将新元素插入到到已有数组中的某个位置
+ * 添加新元素会生成一个新的数组,不影响原数组
+ * 如果插入位置为为负数,从原数组从后向前计数,若大于原数组长度,则空白处用null填充 + * + * @param 数组元素类型 + * @param array 已有数组 + * @param index 插入位置,此位置为对应此位置元素之前的空档 + * @param newElements 新元素 + * @return 新数组 + * @since 4.0.8 + */ + @SuppressWarnings({"unchecked", "SuspiciousSystemArraycopy"}) + public static Object insert(Object array, int index, T... newElements) { + if (isEmpty(newElements)) { + return array; + } + if (isEmpty(array)) { + return newElements; + } + + final int len = length(array); + if (index < 0) { + index = (index % len) + len; + } + + // 已有数组的元素类型 + final Class originComponentType = array.getClass().getComponentType(); + Object newEleArr = newElements; + // 如果 已有数组的元素类型是 原始类型,则需要转换 新元素数组 为该类型,避免ArrayStoreException + if (originComponentType.isPrimitive()) { + newEleArr = Convert.convert(array.getClass(), newElements); + } + final Object result = Array.newInstance(originComponentType, Math.max(len, index) + newElements.length); + System.arraycopy(array, 0, result, 0, Math.min(len, index)); + System.arraycopy(newEleArr, 0, result, index, newElements.length); + if (index < len) { + System.arraycopy(array, index, result, index + newElements.length, len - index); + } + return result; + } + + /** + * 生成一个新的重新设置大小的数组
+ * 调整大小后拷贝原数组到新数组下。扩大则占位前N个位置,缩小则截断 + * + * @param 数组元素类型 + * @param data 原数组 + * @param newSize 新的数组大小 + * @param componentType 数组元素类型 + * @return 调整后的新数组 + */ + public static T[] resize(T[] data, int newSize, Class componentType) { + if (newSize < 0) { + return data; + } + + final T[] newArray = newArray(componentType, newSize); + if (newSize > 0 && isNotEmpty(data)) { + System.arraycopy(data, 0, newArray, 0, Math.min(data.length, newSize)); + } + return newArray; + } + + /** + * 生成一个新的重新设置大小的数组
+ * 调整大小后拷贝原数组到新数组下。扩大则占位前N个位置,其它位置补充0,缩小则截断 + * + * @param array 原数组 + * @param newSize 新的数组大小 + * @return 调整后的新数组 + * @since 4.6.7 + */ + public static Object resize(Object array, int newSize) { + if (newSize < 0) { + return array; + } + if (null == array) { + return null; + } + final int length = length(array); + final Object newArray = Array.newInstance(array.getClass().getComponentType(), newSize); + if (newSize > 0 && isNotEmpty(array)) { + //noinspection SuspiciousSystemArraycopy + System.arraycopy(array, 0, newArray, 0, Math.min(length, newSize)); + } + return newArray; + } + + /** + * 生成一个新的重新设置大小的数组
+ * 新数组的类型为原数组的类型,调整大小后拷贝原数组到新数组下。扩大则占位前N个位置,缩小则截断 + * + * @param 数组元素类型 + * @param buffer 原数组 + * @param newSize 新的数组大小 + * @return 调整后的新数组 + */ + public static T[] resize(T[] buffer, int newSize) { + return resize(buffer, newSize, buffer.getClass().getComponentType()); + } + + /** + * 将多个数组合并在一起
+ * 忽略null的数组 + * + * @param 数组元素类型 + * @param arrays 数组集合 + * @return 合并后的数组 + */ + @SafeVarargs + public static T[] addAll(T[]... arrays) { + if (arrays.length == 1) { + return arrays[0]; + } + + int length = 0; + for (T[] array : arrays) { + if (null != array) { + length += array.length; + } + } + T[] result = newArray(arrays.getClass().getComponentType().getComponentType(), length); + + length = 0; + for (T[] array : arrays) { + if (null != array) { + System.arraycopy(array, 0, result, length, array.length); + length += array.length; + } + } + return result; + } + + /** + * 包装 {@link System#arraycopy(Object, int, Object, int, int)}
+ * 数组复制 + * + * @param src 源数组 + * @param srcPos 源数组开始位置 + * @param dest 目标数组 + * @param destPos 目标数组开始位置 + * @param length 拷贝数组长度 + * @return 目标数组 + * @since 3.0.6 + */ + public static Object copy(Object src, int srcPos, Object dest, int destPos, int length) { + //noinspection SuspiciousSystemArraycopy + System.arraycopy(src, srcPos, dest, destPos, length); + return dest; + } + + /** + * 包装 {@link System#arraycopy(Object, int, Object, int, int)}
+ * 数组复制,缘数组和目标数组都是从位置0开始复制 + * + * @param src 源数组 + * @param dest 目标数组 + * @param length 拷贝数组长度 + * @return 目标数组 + * @since 3.0.6 + */ + public static Object copy(Object src, Object dest, int length) { + //noinspection SuspiciousSystemArraycopy + System.arraycopy(src, 0, dest, 0, length); + return dest; + } + + /** + * 克隆数组 + * + * @param 数组元素类型 + * @param array 被克隆的数组 + * @return 新数组 + */ + public static T[] clone(T[] array) { + if (array == null) { + return null; + } + return array.clone(); + } + + /** + * 克隆数组,如果非数组返回{@code null} + * + * @param 数组元素类型 + * @param obj 数组对象 + * @return 克隆后的数组对象 + */ + @SuppressWarnings("unchecked") + public static T clone(final T obj) { + if (null == obj) { + return null; + } + if (isArray(obj)) { + final Object result; + final Class componentType = obj.getClass().getComponentType(); + if (componentType.isPrimitive()) {// 原始类型 + int length = Array.getLength(obj); + result = Array.newInstance(componentType, length); + while (length-- > 0) { + Array.set(result, length, Array.get(obj, length)); + } + } else { + result = ((Object[]) obj).clone(); + } + return (T) result; + } + return null; + } + + /** + * 编辑数组
+ * 编辑过程通过传入的Editor实现来返回需要的元素内容,这个Editor实现可以实现以下功能: + * + *
+	 * 1、过滤出需要的对象,如果返回{@code null}表示这个元素对象抛弃
+	 * 2、修改元素对象,返回集合中为修改后的对象
+	 * 
+ *

+ * + * @param 数组元素类型 + * @param array 数组 + * @param editor 编辑器接口,{@code null}返回原集合 + * @return 编辑后的数组 + * @since 5.3.3 + */ + public static T[] edit(T[] array, Editor editor) { + if (null == editor) { + return array; + } + + final ArrayList list = new ArrayList<>(array.length); + T modified; + for (T t : array) { + modified = editor.edit(t); + if (null != modified) { + list.add(modified); + } + } + final T[] result = newArray(array.getClass().getComponentType(), list.size()); + return list.toArray(result); + } + + /** + * 过滤
+ * 过滤过程通过传入的Filter实现来过滤返回需要的元素内容,这个Filter实现可以实现以下功能: + * + *

+	 * 1、过滤出需要的对象,{@link Filter#accept(Object)}方法返回true的对象将被加入结果集合中
+	 * 
+ * + * @param 数组元素类型 + * @param array 数组 + * @param filter 过滤器接口,用于定义过滤规则,{@code null}返回原集合 + * @return 过滤后的数组 + * @since 3.2.1 + */ + public static T[] filter(T[] array, Filter filter) { + if (null == array || null == filter) { + return array; + } + return edit(array, t -> filter.accept(t) ? t : null); + } + + /** + * 去除{@code null} 元素 + * + * @param 数组元素类型 + * @param array 数组 + * @return 处理后的数组 + * @since 3.2.2 + */ + public static T[] removeNull(T[] array) { + return edit(array, t -> { + // 返回null便不加入集合 + return t; + }); + } + + /** + * 去除{@code null}或者"" 元素 + * + * @param 数组元素类型 + * @param array 数组 + * @return 处理后的数组 + * @since 3.2.2 + */ + public static T[] removeEmpty(T[] array) { + return filter(array, StrUtil::isNotEmpty); + } + + /** + * 去除{@code null}或者""或者空白字符串 元素 + * + * @param 数组元素类型 + * @param array 数组 + * @return 处理后的数组 + * @since 3.2.2 + */ + public static T[] removeBlank(T[] array) { + return filter(array, StrUtil::isNotBlank); + } + + /** + * 数组元素中的null转换为"" + * + * @param array 数组 + * @return 新数组 + * @since 3.2.1 + */ + public static String[] nullToEmpty(String[] array) { + return edit(array, t -> null == t ? StrUtil.EMPTY : t); + } + + /** + * 映射键值(参考Python的zip()函数)
+ * 例如:
+ * keys = [a,b,c,d]
+ * values = [1,2,3,4]
+ * 则得到的Map是 {a=1, b=2, c=3, d=4}
+ * 如果两个数组长度不同,则只对应最短部分 + * + * @param Key类型 + * @param Value类型 + * @param keys 键列表 + * @param values 值列表 + * @param isOrder 是否有序 + * @return Map + * @since 3.0.4 + */ + public static Map zip(K[] keys, V[] values, boolean isOrder) { + if (isEmpty(keys) || isEmpty(values)) { + return null; + } + + final int size = Math.min(keys.length, values.length); + final Map map = MapUtil.newHashMap(size, isOrder); + for (int i = 0; i < size; i++) { + map.put(keys[i], values[i]); + } + + return map; + } + + /** + * 映射键值(参考Python的zip()函数),返回Map无序
+ * 例如:
+ * keys = [a,b,c,d]
+ * values = [1,2,3,4]
+ * 则得到的Map是 {a=1, b=2, c=3, d=4}
+ * 如果两个数组长度不同,则只对应最短部分 + * + * @param Key类型 + * @param Value类型 + * @param keys 键列表 + * @param values 值列表 + * @return Map + */ + public static Map zip(K[] keys, V[] values) { + return zip(keys, values, false); + } + + // ------------------------------------------------------------------- indexOf and lastIndexOf and contains + + /** + * 返回数组中指定元素所在位置,未找到返回{@link #INDEX_NOT_FOUND} + * + * @param 数组类型 + * @param array 数组 + * @param value 被检查的元素 + * @param beginIndexInclude 检索开始的位置 + * @return 数组中指定元素所在位置,未找到返回{@link #INDEX_NOT_FOUND} + * @since 3.0.7 + */ + public static int indexOf(T[] array, Object value, int beginIndexInclude) { + return matchIndex((obj) -> ObjectUtil.equal(value, obj), beginIndexInclude, array); + } + + /** + * 返回数组中指定元素所在位置,未找到返回{@link #INDEX_NOT_FOUND} + * + * @param 数组类型 + * @param array 数组 + * @param value 被检查的元素 + * @return 数组中指定元素所在位置,未找到返回{@link #INDEX_NOT_FOUND} + * @since 3.0.7 + */ + public static int indexOf(T[] array, Object value) { + return matchIndex((obj) -> ObjectUtil.equal(value, obj), array); + } + + /** + * 返回数组中指定元素所在位置,忽略大小写,未找到返回{@link #INDEX_NOT_FOUND} + * + * @param array 数组 + * @param value 被检查的元素 + * @return 数组中指定元素所在位置,未找到返回{@link #INDEX_NOT_FOUND} + * @since 3.1.2 + */ + public static int indexOfIgnoreCase(CharSequence[] array, CharSequence value) { + if (null != array) { + for (int i = 0; i < array.length; i++) { + if (StrUtil.equalsIgnoreCase(array[i], value)) { + return i; + } + } + } + return INDEX_NOT_FOUND; + } + + /** + * 返回数组中指定元素所在最后的位置,未找到返回{@link #INDEX_NOT_FOUND} + * + * @param 数组类型 + * @param array 数组 + * @param value 被检查的元素 + * @return 数组中指定元素所在位置,未找到返回{@link #INDEX_NOT_FOUND} + * @since 3.0.7 + */ + public static int lastIndexOf(T[] array, Object value) { + if (isEmpty(array)) { + return INDEX_NOT_FOUND; + } + return lastIndexOf(array, value, array.length - 1); + } + + /** + * 返回数组中指定元素所在最后的位置,未找到返回{@link #INDEX_NOT_FOUND} + * + * @param 数组类型 + * @param array 数组 + * @param value 被检查的元素 + * @param endInclude 查找方式为从后向前查找,查找的数组结束位置,一般为array.length-1 + * @return 数组中指定元素所在位置,未找到返回{@link #INDEX_NOT_FOUND} + * @since 5.7.3 + */ + public static int lastIndexOf(T[] array, Object value, int endInclude) { + if (isNotEmpty(array)) { + for (int i = endInclude; i >= 0; i--) { + if (ObjectUtil.equal(value, array[i])) { + return i; + } + } + } + return INDEX_NOT_FOUND; + } + + /** + * 数组中是否包含元素 + * + * @param 数组元素类型 + * @param array 数组 + * @param value 被检查的元素 + * @return 是否包含 + */ + public static boolean contains(T[] array, T value) { + return indexOf(array, value) > INDEX_NOT_FOUND; + } + + /** + * 数组中是否包含指定元素中的任意一个 + * + * @param 数组元素类型 + * @param array 数组 + * @param values 被检查的多个元素 + * @return 是否包含指定元素中的任意一个 + * @since 4.1.20 + */ + @SuppressWarnings("unchecked") + public static boolean containsAny(T[] array, T... values) { + for (T value : values) { + if (contains(array, value)) { + return true; + } + } + return false; + } + + /** + * 数组中是否包含指定元素中的全部 + * + * @param 数组元素类型 + * @param array 数组 + * @param values 被检查的多个元素 + * @return 是否包含指定元素中的全部 + * @since 5.4.7 + */ + @SuppressWarnings("unchecked") + public static boolean containsAll(T[] array, T... values) { + for (T value : values) { + if (!contains(array, value)) { + return false; + } + } + return true; + } + + /** + * 数组中是否包含元素,忽略大小写 + * + * @param array 数组 + * @param value 被检查的元素 + * @return 是否包含 + * @since 3.1.2 + */ + public static boolean containsIgnoreCase(CharSequence[] array, CharSequence value) { + return indexOfIgnoreCase(array, value) > INDEX_NOT_FOUND; + } + + // ------------------------------------------------------------------- Wrap and unwrap + + /** + * 包装数组对象 + * + * @param obj 对象,可以是对象数组或者基本类型数组 + * @return 包装类型数组或对象数组 + * @throws UtilException 对象为非数组 + */ + public static Object[] wrap(Object obj) { + if (null == obj) { + return null; + } + if (isArray(obj)) { + try { + return (Object[]) obj; + } catch (Exception e) { + final String className = obj.getClass().getComponentType().getName(); + switch (className) { + case "long": + return wrap((long[]) obj); + case "int": + return wrap((int[]) obj); + case "short": + return wrap((short[]) obj); + case "char": + return wrap((char[]) obj); + case "byte": + return wrap((byte[]) obj); + case "boolean": + return wrap((boolean[]) obj); + case "float": + return wrap((float[]) obj); + case "double": + return wrap((double[]) obj); + default: + throw new UtilException(e); + } + } + } + throw new UtilException(StrUtil.format("[{}] is not Array!", obj.getClass())); + } + + /** + * 对象是否为数组对象 + * + * @param obj 对象 + * @return 是否为数组对象,如果为{@code null} 返回false + */ + public static boolean isArray(Object obj) { + return null != obj && obj.getClass().isArray(); + } + + /** + * 获取数组对象中指定index的值,支持负数,例如-1表示倒数第一个值
+ * 如果数组下标越界,返回null + * + * @param 数组元素类型 + * @param array 数组对象 + * @param index 下标,支持负数 + * @return 值 + * @since 4.0.6 + */ + @SuppressWarnings("unchecked") + public static T get(Object array, int index) { + if (null == array) { + return null; + } + + if (index < 0) { + index += Array.getLength(array); + } + try { + return (T) Array.get(array, index); + } catch (ArrayIndexOutOfBoundsException e) { + return null; + } + } + + /** + * 获取数组中指定多个下标元素值,组成新数组 + * + * @param 数组元素类型 + * @param array 数组,如果提供为{@code null}则返回{@code null} + * @param indexes 下标列表 + * @return 结果 + */ + public static T[] getAny(Object array, int... indexes) { + if (null == array) { + return null; + } + if(null == indexes){ + return newArray(array.getClass().getComponentType(), 0); + } + + final T[] result = newArray(array.getClass().getComponentType(), indexes.length); + for (int i = 0; i < indexes.length; i++) { + result[i] = ArrayUtil.get(array, indexes[i]); + } + return result; + } + + /** + * 获取子数组 + * + * @param 数组元素类型 + * @param array 数组 + * @param start 开始位置(包括) + * @param end 结束位置(不包括) + * @return 新的数组 + * @see Arrays#copyOfRange(Object[], int, int) + * @since 4.2.2 + */ + public static T[] sub(T[] array, int start, int end) { + int length = length(array); + if (start < 0) { + start += length; + } + if (end < 0) { + end += length; + } + if (start == length) { + return newArray(array.getClass().getComponentType(), 0); + } + if (start > end) { + int tmp = start; + start = end; + end = tmp; + } + if (end > length) { + if (start >= length) { + return newArray(array.getClass().getComponentType(), 0); + } + end = length; + } + return Arrays.copyOfRange(array, start, end); + } + + /** + * 获取子数组 + * + * @param array 数组 + * @param start 开始位置(包括) + * @param end 结束位置(不包括) + * @return 新的数组 + * @since 4.0.6 + */ + public static Object[] sub(Object array, int start, int end) { + return sub(array, start, end, 1); + } + + /** + * 获取子数组 + * + * @param array 数组 + * @param start 开始位置(包括) + * @param end 结束位置(不包括) + * @param step 步进 + * @return 新的数组 + * @since 4.0.6 + */ + public static Object[] sub(Object array, int start, int end, int step) { + int length = length(array); + if (start < 0) { + start += length; + } + if (end < 0) { + end += length; + } + if (start == length) { + return new Object[0]; + } + if (start > end) { + int tmp = start; + start = end; + end = tmp; + } + if (end > length) { + if (start >= length) { + return new Object[0]; + } + end = length; + } + + if (step <= 1) { + step = 1; + } + + final ArrayList list = new ArrayList<>(); + for (int i = start; i < end; i += step) { + list.add(get(array, i)); + } + + return list.toArray(); + } + + /** + * 数组或集合转String + * + * @param obj 集合或数组对象 + * @return 数组字符串,与集合转字符串格式相同 + */ + public static String toString(Object obj) { + if (null == obj) { + return null; + } + + if (obj instanceof long[]) { + return Arrays.toString((long[]) obj); + } else if (obj instanceof int[]) { + return Arrays.toString((int[]) obj); + } else if (obj instanceof short[]) { + return Arrays.toString((short[]) obj); + } else if (obj instanceof char[]) { + return Arrays.toString((char[]) obj); + } else if (obj instanceof byte[]) { + return Arrays.toString((byte[]) obj); + } else if (obj instanceof boolean[]) { + return Arrays.toString((boolean[]) obj); + } else if (obj instanceof float[]) { + return Arrays.toString((float[]) obj); + } else if (obj instanceof double[]) { + return Arrays.toString((double[]) obj); + } else if (ArrayUtil.isArray(obj)) { + // 对象数组 + try { + return Arrays.deepToString((Object[]) obj); + } catch (Exception ignore) { + //ignore + } + } + + return obj.toString(); + } + + /** + * 获取数组长度
+ * 如果参数为{@code null},返回0 + * + *
+	 * ArrayUtil.length(null)            = 0
+	 * ArrayUtil.length([])              = 0
+	 * ArrayUtil.length([null])          = 1
+	 * ArrayUtil.length([true, false])   = 2
+	 * ArrayUtil.length([1, 2, 3])       = 3
+	 * ArrayUtil.length(["a", "b", "c"]) = 3
+	 * 
+ * + * @param array 数组对象 + * @return 数组长度 + * @throws IllegalArgumentException 如果参数不为数组,抛出此异常 + * @see Array#getLength(Object) + * @since 3.0.8 + */ + public static int length(Object array) throws IllegalArgumentException { + if (null == array) { + return 0; + } + return Array.getLength(array); + } + + /** + * 以 conjunction 为分隔符将数组转换为字符串 + * + * @param 被处理的集合 + * @param array 数组 + * @param conjunction 分隔符 + * @return 连接后的字符串 + */ + public static String join(T[] array, CharSequence conjunction) { + return join(array, conjunction, null, null); + } + + /** + * 以 conjunction 为分隔符将数组转换为字符串 + * + * @param 被处理的集合 + * @param array 数组 + * @param delimiter 分隔符 + * @param prefix 每个元素添加的前缀,null表示不添加 + * @param suffix 每个元素添加的后缀,null表示不添加 + * @return 连接后的字符串 + * @since 4.0.10 + */ + public static String join(T[] array, CharSequence delimiter, String prefix, String suffix) { + if (null == array) { + return null; + } + + return StrJoiner.of(delimiter, prefix, suffix) + // 每个元素都添加前后缀 + .setWrapElement(true) + .append(array) + .toString(); + } + + /** + * 以 conjunction 为分隔符将数组转换为字符串 + * + * @param 被处理的集合 + * @param array 数组 + * @param conjunction 分隔符 + * @param editor 每个元素的编辑器,null表示不编辑 + * @return 连接后的字符串 + * @since 5.3.3 + */ + public static String join(T[] array, CharSequence conjunction, Editor editor) { + return StrJoiner.of(conjunction).append(array, (t) -> String.valueOf(editor.edit(t))).toString(); + } + + /** + * 以 conjunction 为分隔符将数组转换为字符串 + * + * @param array 数组 + * @param conjunction 分隔符 + * @return 连接后的字符串 + */ + public static String join(Object array, CharSequence conjunction) { + if (null == array) { + return null; + } + if (!isArray(array)) { + throw new IllegalArgumentException(StrUtil.format("[{}] is not a Array!", array.getClass())); + } + + return StrJoiner.of(conjunction).append(array).toString(); + } + + /** + * {@link ByteBuffer} 转byte数组 + * + * @param bytebuffer {@link ByteBuffer} + * @return byte数组 + * @since 3.0.1 + */ + public static byte[] toArray(ByteBuffer bytebuffer) { + if (bytebuffer.hasArray()) { + return Arrays.copyOfRange(bytebuffer.array(), bytebuffer.position(), bytebuffer.limit()); + } else { + int oldPosition = bytebuffer.position(); + bytebuffer.position(0); + int size = bytebuffer.limit(); + byte[] buffers = new byte[size]; + bytebuffer.get(buffers); + bytebuffer.position(oldPosition); + return buffers; + } + } + + /** + * 将集合转为数组 + * + * @param 数组元素类型 + * @param iterator {@link Iterator} + * @param componentType 集合元素类型 + * @return 数组 + * @since 3.0.9 + */ + public static T[] toArray(Iterator iterator, Class componentType) { + return toArray(CollUtil.newArrayList(iterator), componentType); + } + + /** + * 将集合转为数组 + * + * @param 数组元素类型 + * @param iterable {@link Iterable} + * @param componentType 集合元素类型 + * @return 数组 + * @since 3.0.9 + */ + public static T[] toArray(Iterable iterable, Class componentType) { + return toArray(CollectionUtil.toCollection(iterable), componentType); + } + + /** + * 将集合转为数组 + * + * @param 数组元素类型 + * @param collection 集合 + * @param componentType 集合元素类型 + * @return 数组 + * @since 3.0.9 + */ + public static T[] toArray(Collection collection, Class componentType) { + return collection.toArray(newArray(componentType, 0)); + } + + // ---------------------------------------------------------------------- remove + + /** + * 移除数组中对应位置的元素
+ * copy from commons-lang + * + * @param 数组元素类型 + * @param array 数组对象,可以是对象数组,也可以原始类型数组 + * @param index 位置,如果位置小于0或者大于长度,返回原数组 + * @return 去掉指定元素后的新数组或原数组 + * @throws IllegalArgumentException 参数对象不为数组对象 + * @since 3.0.8 + */ + @SuppressWarnings("unchecked") + public static T[] remove(T[] array, int index) throws IllegalArgumentException { + return (T[]) remove((Object) array, index); + } + + // ---------------------------------------------------------------------- removeEle + + /** + * 移除数组中指定的元素
+ * 只会移除匹配到的第一个元素 copy from commons-lang + * + * @param 数组元素类型 + * @param array 数组对象,可以是对象数组,也可以原始类型数组 + * @param element 要移除的元素 + * @return 去掉指定元素后的新数组或原数组 + * @throws IllegalArgumentException 参数对象不为数组对象 + * @since 3.0.8 + */ + public static T[] removeEle(T[] array, T element) throws IllegalArgumentException { + return remove(array, indexOf(array, element)); + } + + // ---------------------------------------------------------------------- Reverse array + + /** + * 反转数组,会变更原数组 + * + * @param 数组元素类型 + * @param array 数组,会变更 + * @param startIndexInclusive 开始位置(包含) + * @param endIndexExclusive 结束位置(不包含) + * @return 变更后的原数组 + * @since 3.0.9 + */ + public static T[] reverse(T[] array, final int startIndexInclusive, final int endIndexExclusive) { + if (isEmpty(array)) { + return array; + } + int i = Math.max(startIndexInclusive, 0); + int j = Math.min(array.length, endIndexExclusive) - 1; + T tmp; + while (j > i) { + tmp = array[j]; + array[j] = array[i]; + array[i] = tmp; + j--; + i++; + } + return array; + } + + /** + * 反转数组,会变更原数组 + * + * @param 数组元素类型 + * @param array 数组,会变更 + * @return 变更后的原数组 + * @since 3.0.9 + */ + public static T[] reverse(T[] array) { + return reverse(array, 0, array.length); + } + + // ------------------------------------------------------------------------------------------------------------ min and max + + /** + * 取最小值 + * + * @param 元素类型 + * @param numberArray 数字数组 + * @return 最小值 + * @since 3.0.9 + */ + public static > T min(T[] numberArray) { + return min(numberArray, null); + } + + /** + * 取最小值 + * + * @param 元素类型 + * @param numberArray 数字数组 + * @param comparator 比较器,null按照默认比较 + * @return 最小值 + * @since 5.3.4 + */ + public static > T min(T[] numberArray, Comparator comparator) { + if (isEmpty(numberArray)) { + throw new IllegalArgumentException("Number array must not empty !"); + } + T min = numberArray[0]; + for (T t : numberArray) { + if (CompareUtil.compare(min, t, comparator) > 0) { + min = t; + } + } + return min; + } + + /** + * 取最大值 + * + * @param 元素类型 + * @param numberArray 数字数组 + * @return 最大值 + * @since 3.0.9 + */ + public static > T max(T[] numberArray) { + return max(numberArray, null); + } + + /** + * 取最大值 + * + * @param 元素类型 + * @param numberArray 数字数组 + * @param comparator 比较器,null表示默认比较器 + * @return 最大值 + * @since 5.3.4 + */ + public static > T max(T[] numberArray, Comparator comparator) { + if (isEmpty(numberArray)) { + throw new IllegalArgumentException("Number array must not empty !"); + } + T max = numberArray[0]; + for (int i = 1; i < numberArray.length; i++) { + if (CompareUtil.compare(max, numberArray[i], comparator) < 0) { + max = numberArray[i]; + } + } + return max; + } + + // 使用Fisher–Yates洗牌算法,以线性时间复杂度打乱数组顺序 + + /** + * 打乱数组顺序,会变更原数组 + * + * @param 元素类型 + * @param array 数组,会变更 + * @return 打乱后的数组 + * @author FengBaoheng + * @since 5.5.2 + */ + public static T[] shuffle(T[] array) { + return shuffle(array, RandomUtil.getRandom()); + } + + /** + * 打乱数组顺序,会变更原数组 + * + * @param 元素类型 + * @param array 数组,会变更 + * @param random 随机数生成器 + * @return 打乱后的数组 + * @author FengBaoheng + * @since 5.5.2 + */ + public static T[] shuffle(T[] array, Random random) { + if (array == null || random == null || array.length <= 1) { + return array; + } + + for (int i = array.length; i > 1; i--) { + swap(array, i - 1, random.nextInt(i)); + } + + return array; + } + + /** + * 交换数组中两个位置的值 + * + * @param 元素类型 + * @param array 数组 + * @param index1 位置1 + * @param index2 位置2 + * @return 交换后的数组,与传入数组为同一对象 + * @since 4.0.7 + */ + public static T[] swap(T[] array, int index1, int index2) { + if (isEmpty(array)) { + throw new IllegalArgumentException("Array must not empty !"); + } + T tmp = array[index1]; + array[index1] = array[index2]; + array[index2] = tmp; + return array; + } + + /** + * 交换数组中两个位置的值 + * + * @param array 数组对象 + * @param index1 位置1 + * @param index2 位置2 + * @return 交换后的数组,与传入数组为同一对象 + * @since 4.0.7 + */ + public static Object swap(Object array, int index1, int index2) { + if (isEmpty(array)) { + throw new IllegalArgumentException("Array must not empty !"); + } + Object tmp = get(array, index1); + Array.set(array, index1, Array.get(array, index2)); + Array.set(array, index2, tmp); + return array; + } + + /** + * 计算{@code null}或空元素对象的个数,通过{@link ObjectUtil#isEmpty(Object)} 判断元素 + * + * @param args 被检查的对象,一个或者多个 + * @return 存在{@code null}的数量 + * @since 4.5.18 + */ + public static int emptyCount(Object... args) { + int count = 0; + if (isNotEmpty(args)) { + for (Object element : args) { + if (ObjectUtil.isEmpty(element)) { + count++; + } + } + } + return count; + } + + /** + * 是否存在{@code null}或空对象,通过{@link ObjectUtil#isEmpty(Object)} 判断元素 + * + * @param args 被检查对象 + * @return 是否存在 + * @since 4.5.18 + */ + public static boolean hasEmpty(Object... args) { + if (isNotEmpty(args)) { + for (Object element : args) { + if (ObjectUtil.isEmpty(element)) { + return true; + } + } + } + return false; + } + + /** + * 是否存都为{@code null}或空对象,通过{@link ObjectUtil#isEmpty(Object)} 判断元素 + * + * @param args 被检查的对象,一个或者多个 + * @return 是否都为空 + * @since 4.5.18 + */ + public static boolean isAllEmpty(Object... args) { + for (Object obj : args) { + if (!ObjectUtil.isEmpty(obj)) { + return false; + } + } + return true; + } + + /** + * 是否存都不为{@code null}或空对象,通过{@link ObjectUtil#isEmpty(Object)} 判断元素 + * + * @param args 被检查的对象,一个或者多个 + * @return 是否都不为空 + * @since 4.5.18 + */ + public static boolean isAllNotEmpty(Object... args) { + return !hasEmpty(args); + } + + /** + * 多个字段是否全部不为null + * + * @param 数组元素类型 + * @param array 被检查的数组 + * @return 多个字段是否全部不为null + * @since 5.4.0 + */ + @SuppressWarnings("unchecked") + public static boolean isAllNotNull(T... array) { + return !hasNull(array); + } + + /** + * 去重数组中的元素,去重后生成新的数组,原数组不变
+ * 此方法通过{@link LinkedHashSet} 去重 + * + * @param 数组元素类型 + * @param array 数组 + * @return 去重后的数组 + */ + @SuppressWarnings("unchecked") + public static T[] distinct(T[] array) { + if (isEmpty(array)) { + return array; + } + + final Set set = new LinkedHashSet<>(array.length, 1); + Collections.addAll(set, array); + return toArray(set, (Class) getComponentType(array)); + } + + /** + * 去重数组中的元素,去重后生成新的数组,原数组不变
+ * 此方法通过{@link LinkedHashSet} 去重 + * + * @param 数组元素类型 + * @param 唯一键类型 + * @param array 数组 + * @param uniqueGenerator 唯一键生成器 + * @param override 是否覆盖模式,如果为{@code true},加入的新值会覆盖相同key的旧值,否则会忽略新加值 + * @return 去重后的数组 + * @since 5.8.0 + */ + @SuppressWarnings("unchecked") + public static T[] distinct(T[] array, Function uniqueGenerator, boolean override) { + if (isEmpty(array)) { + return array; + } + + final UniqueKeySet set = new UniqueKeySet<>(true, uniqueGenerator); + if(override){ + Collections.addAll(set, array); + } else{ + for (T t : array) { + set.addIfAbsent(t); + } + } + return toArray(set, (Class) getComponentType(array)); + } + + /** + * 按照指定规则,将一种类型的数组转换为另一种类型 + * + * @param array 被转换的数组 + * @param targetComponentType 目标的元素类型 + * @param func 转换规则函数 + * @param 原数组类型 + * @param 目标数组类型 + * @return 转换后的数组 + * @since 5.4.2 + */ + public static R[] map(T[] array, Class targetComponentType, Function func) { + final R[] result = newArray(targetComponentType, array.length); + for (int i = 0; i < array.length; i++) { + result[i] = func.apply(array[i]); + } + return result; + } + + /** + * 按照指定规则,将一种类型的数组转换为另一种类型 + * + * @param array 被转换的数组 + * @param targetComponentType 目标的元素类型 + * @param func 转换规则函数 + * @param 原数组类型 + * @param 目标数组类型 + * @return 转换后的数组 + * @since 5.5.8 + */ + public static R[] map(Object array, Class targetComponentType, Function func) { + final int length = length(array); + final R[] result = newArray(targetComponentType, length); + for (int i = 0; i < length; i++) { + result[i] = func.apply(get(array, i)); + } + return result; + } + + /** + * 按照指定规则,将一种类型的数组元素提取后转换为{@link List} + * + * @param array 被转换的数组 + * @param func 转换规则函数 + * @param 原数组类型 + * @param 目标数组类型 + * @return 转换后的数组 + * @since 5.5.7 + */ + public static List map(T[] array, Function func) { + return Arrays.stream(array).map(func).collect(Collectors.toList()); + } + + /** + * 按照指定规则,将一种类型的数组元素提取后转换为{@link Set} + * + * @param array 被转换的数组 + * @param func 转换规则函数 + * @param 原数组类型 + * @param 目标数组类型 + * @return 转换后的数组 + * @since 5.8.0 + */ + public static Set mapToSet(T[] array, Function func) { + return Arrays.stream(array).map(func).collect(Collectors.toSet()); + } + + /** + * 判断两个数组是否相等,判断依据包括数组长度和每个元素都相等。 + * + * @param array1 数组1 + * @param array2 数组2 + * @return 是否相等 + * @since 5.4.2 + */ + public static boolean equals(Object array1, Object array2) { + if (array1 == array2) { + return true; + } + if (hasNull(array1, array2)) { + return false; + } + + Assert.isTrue(isArray(array1), "First is not a Array !"); + Assert.isTrue(isArray(array2), "Second is not a Array !"); + + if (array1 instanceof long[]) { + return Arrays.equals((long[]) array1, (long[]) array2); + } else if (array1 instanceof int[]) { + return Arrays.equals((int[]) array1, (int[]) array2); + } else if (array1 instanceof short[]) { + return Arrays.equals((short[]) array1, (short[]) array2); + } else if (array1 instanceof char[]) { + return Arrays.equals((char[]) array1, (char[]) array2); + } else if (array1 instanceof byte[]) { + return Arrays.equals((byte[]) array1, (byte[]) array2); + } else if (array1 instanceof double[]) { + return Arrays.equals((double[]) array1, (double[]) array2); + } else if (array1 instanceof float[]) { + return Arrays.equals((float[]) array1, (float[]) array2); + } else if (array1 instanceof boolean[]) { + return Arrays.equals((boolean[]) array1, (boolean[]) array2); + } else { + // Not an array of primitives + return Arrays.deepEquals((Object[]) array1, (Object[]) array2); + } + } + + /** + * 查找子数组的位置 + * + * @param array 数组 + * @param subArray 子数组 + * @param 数组元素类型 + * @return 子数组的开始位置,即子数字第一个元素在数组中的位置 + * @since 5.4.8 + */ + public static boolean isSub(T[] array, T[] subArray) { + return indexOfSub(array, subArray) > INDEX_NOT_FOUND; + } + + /** + * 查找子数组的位置 + * + * @param array 数组 + * @param subArray 子数组 + * @param 数组元素类型 + * @return 子数组的开始位置,即子数字第一个元素在数组中的位置 + * @since 5.4.8 + */ + public static int indexOfSub(T[] array, T[] subArray) { + return indexOfSub(array, 0, subArray); + } + + /** + * 查找子数组的位置 + * + * @param array 数组 + * @param beginInclude 查找开始的位置(包含) + * @param subArray 子数组 + * @param 数组元素类型 + * @return 子数组的开始位置,即子数字第一个元素在数组中的位置 + * @since 5.4.8 + */ + public static int indexOfSub(T[] array, int beginInclude, T[] subArray) { + if (isEmpty(array) || isEmpty(subArray) || subArray.length > array.length) { + return INDEX_NOT_FOUND; + } + int firstIndex = indexOf(array, subArray[0], beginInclude); + if (firstIndex < 0 || firstIndex + subArray.length > array.length) { + return INDEX_NOT_FOUND; + } + + for (int i = 0; i < subArray.length; i++) { + if (!ObjectUtil.equal(array[i + firstIndex], subArray[i])) { + return indexOfSub(array, firstIndex + 1, subArray); + } + } + + return firstIndex; + } + + /** + * 查找最后一个子数组的开始位置 + * + * @param array 数组 + * @param subArray 子数组 + * @param 数组元素类型 + * @return 最后一个子数组的开始位置,即子数字第一个元素在数组中的位置 + * @since 5.4.8 + */ + public static int lastIndexOfSub(T[] array, T[] subArray) { + if (isEmpty(array) || isEmpty(subArray)) { + return INDEX_NOT_FOUND; + } + return lastIndexOfSub(array, array.length - 1, subArray); + } + + /** + * 查找最后一个子数组的开始位置 + * + * @param array 数组 + * @param endInclude 查找结束的位置(包含) + * @param subArray 子数组 + * @param 数组元素类型 + * @return 最后一个子数组的开始位置,即子数字第一个元素在数组中的位置 + * @since 5.4.8 + */ + public static int lastIndexOfSub(T[] array, int endInclude, T[] subArray) { + if (isEmpty(array) || isEmpty(subArray) || subArray.length > array.length || endInclude < 0) { + return INDEX_NOT_FOUND; + } + + int firstIndex = lastIndexOf(array, subArray[0]); + if (firstIndex < 0 || firstIndex + subArray.length > array.length) { + return INDEX_NOT_FOUND; + } + + for (int i = 0; i < subArray.length; i++) { + if (!ObjectUtil.equal(array[i + firstIndex], subArray[i])) { + return lastIndexOfSub(array, firstIndex - 1, subArray); + } + } + + return firstIndex; + } + + // O(n)时间复杂度检查数组是否有序 + + /** + * 检查数组是否有序,即comparator.compare(array[i], array[i + 1]) <= 0,若传入空数组或空比较器,则返回false + * + * @param array 数组 + * @param comparator 比较器 + * @param 数组元素类型 + * @return 数组是否有序 + * @author FengBaoheng + * @since 5.5.2 + */ + public static boolean isSorted(T[] array, Comparator comparator) { + if (array == null || comparator == null) { + return false; + } + + for (int i = 0; i < array.length - 1; i++) { + if (comparator.compare(array[i], array[i + 1]) > 0) { + return false; + } + } + return true; + } + + /** + * 检查数组是否升序,即array[i].compareTo(array[i + 1]) <= 0,若传入空数组,则返回false + * + * @param 数组元素类型,该类型需要实现Comparable接口 + * @param array 数组 + * @return 数组是否升序 + * @author FengBaoheng + * @since 5.5.2 + */ + public static > boolean isSorted(T[] array) { + return isSortedASC(array); + } + + + /** + * 检查数组是否升序,即array[i].compareTo(array[i + 1]) <= 0,若传入空数组,则返回false + * + * @param 数组元素类型,该类型需要实现Comparable接口 + * @param array 数组 + * @return 数组是否升序 + * @author FengBaoheng + * @since 5.5.2 + */ + public static > boolean isSortedASC(T[] array) { + if (array == null) { + return false; + } + + for (int i = 0; i < array.length - 1; i++) { + if (array[i].compareTo(array[i + 1]) > 0) { + return false; + } + } + + return true; + } + + /** + * 检查数组是否降序,即array[i].compareTo(array[i + 1]) >= 0,若传入空数组,则返回false + * + * @param 数组元素类型,该类型需要实现Comparable接口 + * @param array 数组 + * @return 数组是否降序 + * @author FengBaoheng + * @since 5.5.2 + */ + public static > boolean isSortedDESC(T[] array) { + if (array == null) { + return false; + } + + for (int i = 0; i < array.length - 1; i++) { + if (array[i].compareTo(array[i + 1]) < 0) { + return false; + } + } + + return true; + } +} diff --git a/src/main/java/cn/hutool/core/util/BooleanUtil.java b/src/main/java/cn/hutool/core/util/BooleanUtil.java new file mode 100644 index 0000000..7666270 --- /dev/null +++ b/src/main/java/cn/hutool/core/util/BooleanUtil.java @@ -0,0 +1,504 @@ +package cn.hutool.core.util; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.convert.Convert; + +import java.util.Set; + +/** + * Boolean类型相关工具类 + * + * @author looly + * @since 4.1.16 + */ +public class BooleanUtil { + + /** 表示为真的字符串 */ + private static final Set TRUE_SET = CollUtil.newHashSet("true", "yes", "y", "t", "ok", "1", "on", "是", "对", "真", "對", "√"); + /** 表示为假的字符串 */ + private static final Set FALSE_SET = CollUtil.newHashSet("false", "no", "n", "f", "0", "off", "否", "错", "假", "錯", "×"); + + /** + * 取相反值 + * + * @param bool Boolean值 + * @return 相反的Boolean值 + */ + public static Boolean negate(Boolean bool) { + if (bool == null) { + return null; + } + return bool ? Boolean.FALSE : Boolean.TRUE; + } + + /** + * 检查 {@code Boolean} 值是否为 {@code true} + * + *
+	 *   BooleanUtil.isTrue(Boolean.TRUE)  = true
+	 *   BooleanUtil.isTrue(Boolean.FALSE) = false
+	 *   BooleanUtil.isTrue(null)          = false
+	 * 
+ * + * @param bool 被检查的Boolean值 + * @return 当值为true且非null时返回{@code true} + */ + public static boolean isTrue(Boolean bool) { + return Boolean.TRUE.equals(bool); + } + + /** + * 检查 {@code Boolean} 值是否为 {@code false} + * + *
+	 *   BooleanUtil.isFalse(Boolean.TRUE)  = false
+	 *   BooleanUtil.isFalse(Boolean.FALSE) = true
+	 *   BooleanUtil.isFalse(null)          = false
+	 * 
+ * + * @param bool 被检查的Boolean值 + * @return 当值为false且非null时返回{@code true} + */ + public static boolean isFalse(Boolean bool) { + return Boolean.FALSE.equals(bool); + } + + /** + * 取相反值 + * + * @param bool Boolean值 + * @return 相反的Boolean值 + */ + public static boolean negate(boolean bool) { + return !bool; + } + + /** + * 转换字符串为boolean值 + * + * @param valueStr 字符串 + * @return boolean值 + */ + public static boolean toBoolean(String valueStr) { + if (StrUtil.isNotBlank(valueStr)) { + valueStr = valueStr.trim().toLowerCase(); + return TRUE_SET.contains(valueStr); + } + return false; + } + + /** + * 转换字符串为boolean值
+ * 如果为["true", "yes", "y", "t", "ok", "1", "on", "是", "对", "真", "對", "√"],返回{@code true}
+ * 如果为["false", "no", "n", "f", "0", "off", "否", "错", "假", "錯", "×"],返回{@code false}
+ * 其他情况返回{@code null} + * + * @param valueStr 字符串 + * @return boolean值 + * @since 5.8.1 + */ + public static Boolean toBooleanObject(String valueStr) { + if (StrUtil.isNotBlank(valueStr)) { + valueStr = valueStr.trim().toLowerCase(); + if(TRUE_SET.contains(valueStr)){ + return true; + } else if(FALSE_SET.contains(valueStr)){ + return false; + } + } + return null; + } + + /** + * boolean值转为int + * + * @param value Boolean值 + * @return int值 + */ + public static int toInt(boolean value) { + return value ? 1 : 0; + } + + /** + * boolean值转为Integer + * + * @param value Boolean值 + * @return Integer值 + */ + public static Integer toInteger(boolean value) { + return toInt(value); + } + + /** + * boolean值转为char + * + * @param value Boolean值 + * @return char值 + */ + public static char toChar(boolean value) { + return (char) toInt(value); + } + + /** + * boolean值转为Character + * + * @param value Boolean值 + * @return Character值 + */ + public static Character toCharacter(boolean value) { + return toChar(value); + } + + /** + * boolean值转为byte + * + * @param value Boolean值 + * @return byte值 + */ + public static byte toByte(boolean value) { + return (byte) toInt(value); + } + + /** + * boolean值转为Byte + * + * @param value Boolean值 + * @return Byte值 + */ + public static Byte toByteObj(boolean value) { + return toByte(value); + } + + /** + * boolean值转为long + * + * @param value Boolean值 + * @return long值 + */ + public static long toLong(boolean value) { + return toInt(value); + } + + /** + * boolean值转为Long + * + * @param value Boolean值 + * @return Long值 + */ + public static Long toLongObj(boolean value) { + return toLong(value); + } + + /** + * boolean值转为short + * + * @param value Boolean值 + * @return short值 + */ + public static short toShort(boolean value) { + return (short) toInt(value); + } + + /** + * boolean值转为Short + * + * @param value Boolean值 + * @return Short值 + */ + public static Short toShortObj(boolean value) { + return toShort(value); + } + + /** + * boolean值转为float + * + * @param value Boolean值 + * @return float值 + */ + public static float toFloat(boolean value) { + return (float) toInt(value); + } + + /** + * boolean值转为Float + * + * @param value Boolean值 + * @return float值 + */ + public static Float toFloatObj(boolean value) { + return toFloat(value); + } + + /** + * boolean值转为double + * + * @param value Boolean值 + * @return double值 + */ + public static double toDouble(boolean value) { + return toInt(value); + } + + /** + * boolean值转为double + * + * @param value Boolean值 + * @return double值 + */ + public static Double toDoubleObj(boolean value) { + return toDouble(value); + } + + /** + * 将boolean转换为字符串 {@code 'true'} 或者 {@code 'false'}. + * + *
+	 *   BooleanUtil.toStringTrueFalse(true)   = "true"
+	 *   BooleanUtil.toStringTrueFalse(false)  = "false"
+	 * 
+ * + * @param bool Boolean值 + * @return {@code 'true'}, {@code 'false'} + */ + public static String toStringTrueFalse(boolean bool) { + return toString(bool, "true", "false"); + } + + /** + * 将boolean转换为字符串 {@code 'on'} 或者 {@code 'off'}. + * + *
+	 *   BooleanUtil.toStringOnOff(true)   = "on"
+	 *   BooleanUtil.toStringOnOff(false)  = "off"
+	 * 
+ * + * @param bool Boolean值 + * @return {@code 'on'}, {@code 'off'} + */ + public static String toStringOnOff(boolean bool) { + return toString(bool, "on", "off"); + } + + /** + * 将boolean转换为字符串 {@code 'yes'} 或者 {@code 'no'}. + * + *
+	 *   BooleanUtil.toStringYesNo(true)   = "yes"
+	 *   BooleanUtil.toStringYesNo(false)  = "no"
+	 * 
+ * + * @param bool Boolean值 + * @return {@code 'yes'}, {@code 'no'} + */ + public static String toStringYesNo(boolean bool) { + return toString(bool, "yes", "no"); + } + + /** + * 将boolean转换为字符串 + * + *
+	 *   BooleanUtil.toString(true, "true", "false")   = "true"
+	 *   BooleanUtil.toString(false, "true", "false")  = "false"
+	 * 
+ * + * @param bool Boolean值 + * @param trueString 当值为 {@code true}时返回此字符串, 可能为 {@code null} + * @param falseString 当值为 {@code false}时返回此字符串, 可能为 {@code null} + * @return 结果值 + */ + public static String toString(boolean bool, String trueString, String falseString) { + return bool ? trueString : falseString; + } + + /** + * 将boolean转换为字符串 + * + *
+	 *   BooleanUtil.toString(true, "true", "false", null) = "true"
+	 *   BooleanUtil.toString(false, "true", "false", null) = "false"
+	 *   BooleanUtil.toString(null, "true", "false", null) = null
+	 * 
+ * + * @param bool Boolean值 + * @param trueString 当值为 {@code true}时返回此字符串, 可能为 {@code null} + * @param falseString 当值为 {@code false}时返回此字符串, 可能为 {@code null} + * @param nullString 当值为 {@code null}时返回此字符串, 可能为 {@code null} + * @return 结果值 + */ + public static String toString(Boolean bool, String trueString, String falseString, String nullString) { + if (bool == null) { + return nullString; + } + return bool ? trueString : falseString; + } + + /** + * 对Boolean数组取与 + * + *
+	 *   BooleanUtil.and(true, true)         = true
+	 *   BooleanUtil.and(false, false)       = false
+	 *   BooleanUtil.and(true, false)        = false
+	 *   BooleanUtil.and(true, true, false)  = false
+	 *   BooleanUtil.and(true, true, true)   = true
+	 * 
+ * + * @param array {@code Boolean}数组 + * @return 取与为真返回{@code true} + */ + public static boolean and(boolean... array) { + if (ArrayUtil.isEmpty(array)) { + throw new IllegalArgumentException("The Array must not be empty !"); + } + for (final boolean element : array) { + if (!element) { + return false; + } + } + return true; + } + + /** + * 对Boolean数组取与 + * + *
+	 *   BooleanUtil.and(Boolean.TRUE, Boolean.TRUE)                 = Boolean.TRUE
+	 *   BooleanUtil.and(Boolean.FALSE, Boolean.FALSE)               = Boolean.FALSE
+	 *   BooleanUtil.and(Boolean.TRUE, Boolean.FALSE)                = Boolean.FALSE
+	 *   BooleanUtil.and(Boolean.TRUE, Boolean.TRUE, Boolean.TRUE)   = Boolean.TRUE
+	 *   BooleanUtil.and(Boolean.FALSE, Boolean.FALSE, Boolean.TRUE) = Boolean.FALSE
+	 *   BooleanUtil.and(Boolean.TRUE, Boolean.FALSE, Boolean.TRUE)  = Boolean.FALSE
+	 * 
+ * + * @param array {@code Boolean}数组 + * @return 取与为真返回{@code true} + */ + public static Boolean andOfWrap(Boolean... array) { + if (ArrayUtil.isEmpty(array)) { + throw new IllegalArgumentException("The Array must not be empty !"); + } + + for (final Boolean b : array) { + if(isFalse(b)){ + return false; + } + } + return true; + } + + /** + * 对Boolean数组取或 + * + *
+	 *   BooleanUtil.or(true, true)          = true
+	 *   BooleanUtil.or(false, false)        = false
+	 *   BooleanUtil.or(true, false)         = true
+	 *   BooleanUtil.or(true, true, false)   = true
+	 *   BooleanUtil.or(true, true, true)    = true
+	 *   BooleanUtil.or(false, false, false) = false
+	 * 
+ * + * @param array {@code Boolean}数组 + * @return 取或为真返回{@code true} + */ + public static boolean or(boolean... array) { + if (ArrayUtil.isEmpty(array)) { + throw new IllegalArgumentException("The Array must not be empty !"); + } + for (final boolean element : array) { + if (element) { + return true; + } + } + return false; + } + + /** + * 对Boolean数组取或 + * + *
+	 *   BooleanUtil.or(Boolean.TRUE, Boolean.TRUE)                  = Boolean.TRUE
+	 *   BooleanUtil.or(Boolean.FALSE, Boolean.FALSE)                = Boolean.FALSE
+	 *   BooleanUtil.or(Boolean.TRUE, Boolean.FALSE)                 = Boolean.TRUE
+	 *   BooleanUtil.or(Boolean.TRUE, Boolean.TRUE, Boolean.TRUE)    = Boolean.TRUE
+	 *   BooleanUtil.or(Boolean.FALSE, Boolean.FALSE, Boolean.TRUE)  = Boolean.TRUE
+	 *   BooleanUtil.or(Boolean.TRUE, Boolean.FALSE, Boolean.TRUE)   = Boolean.TRUE
+	 *   BooleanUtil.or(Boolean.FALSE, Boolean.FALSE, Boolean.FALSE) = Boolean.FALSE
+	 * 
+ * + * @param array {@code Boolean}数组 + * @return 取或为真返回{@code true} + */ + public static Boolean orOfWrap(Boolean... array) { + if (ArrayUtil.isEmpty(array)) { + throw new IllegalArgumentException("The Array must not be empty !"); + } + + for (final Boolean b : array) { + if(isTrue(b)){ + return true; + } + } + return false; + } + + /** + * 对Boolean数组取异或 + * + *
+	 *   BooleanUtil.xor(true, true)   = false
+	 *   BooleanUtil.xor(false, false) = false
+	 *   BooleanUtil.xor(true, false)  = true
+	 *   BooleanUtil.xor(true, true)   = false
+	 *   BooleanUtil.xor(false, false) = false
+	 *   BooleanUtil.xor(true, false)  = true
+	 * 
+ * + * @param array {@code boolean}数组 + * @return 如果异或计算为true返回 {@code true} + */ + public static boolean xor(boolean... array) { + if (ArrayUtil.isEmpty(array)) { + throw new IllegalArgumentException("The Array must not be empty"); + } + + boolean result = false; + for (final boolean element : array) { + result ^= element; + } + + return result; + } + + /** + * 对Boolean数组取异或 + * + *
+	 *   BooleanUtil.xor(new Boolean[] { Boolean.TRUE, Boolean.TRUE })   = Boolean.FALSE
+	 *   BooleanUtil.xor(new Boolean[] { Boolean.FALSE, Boolean.FALSE }) = Boolean.FALSE
+	 *   BooleanUtil.xor(new Boolean[] { Boolean.TRUE, Boolean.FALSE })  = Boolean.TRUE
+	 * 
+ * + * @param array {@code Boolean} 数组 + * @return 异或为真取{@code true} + */ + public static Boolean xorOfWrap(Boolean... array) { + if (ArrayUtil.isEmpty(array)) { + throw new IllegalArgumentException("The Array must not be empty !"); + } + final boolean[] primitive = Convert.convert(boolean[].class, array); + return xor(primitive); + } + + /** + * 给定类是否为Boolean或者boolean + * + * @param clazz 类 + * @return 是否为Boolean或者boolean + * @since 4.5.2 + */ + public static boolean isBoolean(Class clazz) { + return (clazz == Boolean.class || clazz == boolean.class); + } +} diff --git a/src/main/java/cn/hutool/core/util/ByteUtil.java b/src/main/java/cn/hutool/core/util/ByteUtil.java new file mode 100644 index 0000000..8fbab02 --- /dev/null +++ b/src/main/java/cn/hutool/core/util/ByteUtil.java @@ -0,0 +1,489 @@ +package cn.hutool.core.util; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.nio.ByteOrder; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.DoubleAdder; +import java.util.concurrent.atomic.LongAdder; + +/** + * 对数字和字节进行转换。
+ * 假设数据存储是以大端模式存储的:
+ *
    + *
  • byte: 字节类型 占8位二进制 00000000
  • + *
  • char: 字符类型 占2个字节 16位二进制 byte[0] byte[1]
  • + *
  • int : 整数类型 占4个字节 32位二进制 byte[0] byte[1] byte[2] byte[3]
  • + *
  • long: 长整数类型 占8个字节 64位二进制 byte[0] byte[1] byte[2] byte[3] byte[4] byte[5]
  • + *
  • long: 长整数类型 占8个字节 64位二进制 byte[0] byte[1] byte[2] byte[3] byte[4] byte[5] byte[6] byte[7]
  • + *
  • float: 浮点数(小数) 占4个字节 32位二进制 byte[0] byte[1] byte[2] byte[3]
  • + *
  • double: 双精度浮点数(小数) 占8个字节 64位二进制 byte[0] byte[1] byte[2] byte[3] byte[4]byte[5] byte[6] byte[7]
  • + *
+ * 注:注释来自Hanlp,代码提供来自pr#1492@Github + * + * @author looly, hanlp, FULaBUla + * @since 5.6.3 + */ +public class ByteUtil { + + public static final ByteOrder DEFAULT_ORDER = ByteOrder.LITTLE_ENDIAN; + /** + * CPU的字节序 + */ + public static final ByteOrder CPU_ENDIAN = "little".equals(System.getProperty("sun.cpu.endian")) ? ByteOrder.LITTLE_ENDIAN : ByteOrder.BIG_ENDIAN; + + /** + * int转byte + * + * @param intValue int值 + * @return byte值 + */ + public static byte intToByte(int intValue) { + return (byte) intValue; + } + + /** + * byte转无符号int + * + * @param byteValue byte值 + * @return 无符号int值 + * @since 3.2.0 + */ + public static int byteToUnsignedInt(byte byteValue) { + // Java 总是把 byte 当做有符处理;我们可以通过将其和 0xFF 进行二进制与得到它的无符值 + return byteValue & 0xFF; + } + + /** + * byte数组转short
+ * 默认以小端序转换 + * + * @param bytes byte数组 + * @return short值 + */ + public static short bytesToShort(byte[] bytes) { + return bytesToShort(bytes, DEFAULT_ORDER); + } + + /** + * byte数组转short
+ * 自定义端序 + * + * @param bytes byte数组,长度必须为2 + * @param byteOrder 端序 + * @return short值 + */ + public static short bytesToShort(final byte[] bytes, final ByteOrder byteOrder) { + return bytesToShort(bytes, 0, byteOrder); + } + + /** + * byte数组转short
+ * 自定义端序 + * + * @param bytes byte数组,长度必须大于2 + * @param start 开始位置 + * @param byteOrder 端序 + * @return short值 + */ + public static short bytesToShort(final byte[] bytes, final int start, final ByteOrder byteOrder) { + if (ByteOrder.LITTLE_ENDIAN == byteOrder) { + //小端模式,数据的高字节保存在内存的高地址中,而数据的低字节保存在内存的低地址中 + return (short) (bytes[start] & 0xff | (bytes[start + 1] & 0xff) << Byte.SIZE); + } else { + return (short) (bytes[start + 1] & 0xff | (bytes[start] & 0xff) << Byte.SIZE); + } + } + + /** + * short转byte数组
+ * 默认以小端序转换 + * + * @param shortValue short值 + * @return byte数组 + */ + public static byte[] shortToBytes(short shortValue) { + return shortToBytes(shortValue, DEFAULT_ORDER); + } + + /** + * short转byte数组
+ * 自定义端序 + * + * @param shortValue short值 + * @param byteOrder 端序 + * @return byte数组 + */ + public static byte[] shortToBytes(short shortValue, ByteOrder byteOrder) { + byte[] b = new byte[Short.BYTES]; + if (ByteOrder.LITTLE_ENDIAN == byteOrder) { + b[0] = (byte) (shortValue & 0xff); + b[1] = (byte) ((shortValue >> Byte.SIZE) & 0xff); + } else { + b[1] = (byte) (shortValue & 0xff); + b[0] = (byte) ((shortValue >> Byte.SIZE) & 0xff); + } + return b; + } + + /** + * byte[]转int值
+ * 默认以小端序转换 + * + * @param bytes byte数组 + * @return int值 + */ + public static int bytesToInt(byte[] bytes) { + return bytesToInt(bytes, DEFAULT_ORDER); + } + + /** + * byte[]转int值
+ * 自定义端序 + * + * @param bytes byte数组 + * @param byteOrder 端序 + * @return int值 + */ + public static int bytesToInt(byte[] bytes, ByteOrder byteOrder) { + return bytesToInt(bytes, 0, byteOrder); + } + + /** + * byte[]转int值
+ * 自定义端序 + * + * @param bytes byte数组 + * @param start 开始位置(包含) + * @param byteOrder 端序 + * @return int值 + * @since 5.7.21 + */ + public static int bytesToInt(byte[] bytes, int start, ByteOrder byteOrder) { + if (ByteOrder.LITTLE_ENDIAN == byteOrder) { + return bytes[start] & 0xFF | // + (bytes[1 + start] & 0xFF) << 8 | // + (bytes[2 + start] & 0xFF) << 16 | // + (bytes[3 + start] & 0xFF) << 24; // + } else { + return bytes[3 + start] & 0xFF | // + (bytes[2 + start] & 0xFF) << 8 | // + (bytes[1 + start] & 0xFF) << 16 | // + (bytes[start] & 0xFF) << 24; // + } + + } + + /** + * int转byte数组
+ * 默认以小端序转换 + * + * @param intValue int值 + * @return byte数组 + */ + public static byte[] intToBytes(int intValue) { + return intToBytes(intValue, DEFAULT_ORDER); + } + + /** + * int转byte数组
+ * 自定义端序 + * + * @param intValue int值 + * @param byteOrder 端序 + * @return byte数组 + */ + public static byte[] intToBytes(int intValue, ByteOrder byteOrder) { + + if (ByteOrder.LITTLE_ENDIAN == byteOrder) { + return new byte[]{ // + (byte) (intValue & 0xFF), // + (byte) ((intValue >> 8) & 0xFF), // + (byte) ((intValue >> 16) & 0xFF), // + (byte) ((intValue >> 24) & 0xFF) // + }; + + } else { + return new byte[]{ // + (byte) ((intValue >> 24) & 0xFF), // + (byte) ((intValue >> 16) & 0xFF), // + (byte) ((intValue >> 8) & 0xFF), // + (byte) (intValue & 0xFF) // + }; + } + + } + + /** + * long转byte数组
+ * 默认以小端序转换
+ * from: https://stackoverflow.com/questions/4485128/how-do-i-convert-long-to-byte-and-back-in-java + * + * @param longValue long值 + * @return byte数组 + */ + public static byte[] longToBytes(long longValue) { + return longToBytes(longValue, DEFAULT_ORDER); + } + + /** + * long转byte数组
+ * 自定义端序
+ * from: https://stackoverflow.com/questions/4485128/how-do-i-convert-long-to-byte-and-back-in-java + * + * @param longValue long值 + * @param byteOrder 端序 + * @return byte数组 + */ + public static byte[] longToBytes(long longValue, ByteOrder byteOrder) { + byte[] result = new byte[Long.BYTES]; + if (ByteOrder.LITTLE_ENDIAN == byteOrder) { + for (int i = 0; i < result.length; i++) { + result[i] = (byte) (longValue & 0xFF); + longValue >>= Byte.SIZE; + } + } else { + for (int i = (result.length - 1); i >= 0; i--) { + result[i] = (byte) (longValue & 0xFF); + longValue >>= Byte.SIZE; + } + } + return result; + } + + /** + * byte数组转long
+ * 默认以小端序转换
+ * from: https://stackoverflow.com/questions/4485128/how-do-i-convert-long-to-byte-and-back-in-java + * + * @param bytes byte数组 + * @return long值 + */ + public static long bytesToLong(byte[] bytes) { + return bytesToLong(bytes, DEFAULT_ORDER); + } + + /** + * byte数组转long
+ * 自定义端序
+ * from: https://stackoverflow.com/questions/4485128/how-do-i-convert-long-to-byte-and-back-in-java + * + * @param bytes byte数组 + * @param byteOrder 端序 + * @return long值 + */ + public static long bytesToLong(byte[] bytes, ByteOrder byteOrder) { + return bytesToLong(bytes, 0, byteOrder); + } + + /** + * byte数组转long
+ * 自定义端序
+ * from: https://stackoverflow.com/questions/4485128/how-do-i-convert-long-to-byte-and-back-in-java + * + * @param bytes byte数组 + * @param start 计算数组开始位置 + * @param byteOrder 端序 + * @return long值 + * @since 5.7.21 + */ + public static long bytesToLong(byte[] bytes, int start, ByteOrder byteOrder) { + long values = 0; + if (ByteOrder.LITTLE_ENDIAN == byteOrder) { + for (int i = (Long.BYTES - 1); i >= 0; i--) { + values <<= Byte.SIZE; + values |= (bytes[i + start] & 0xff); + } + } else { + for (int i = 0; i < Long.BYTES; i++) { + values <<= Byte.SIZE; + values |= (bytes[i + start] & 0xff); + } + } + + return values; + } + + /** + * float转byte数组,默认以小端序转换
+ * + * @param floatValue float值 + * @return byte数组 + * @since 5.7.18 + */ + public static byte[] floatToBytes(float floatValue) { + return floatToBytes(floatValue, DEFAULT_ORDER); + } + + /** + * float转byte数组,自定义端序
+ * + * @param floatValue float值 + * @param byteOrder 端序 + * @return byte数组 + * @since 5.7.18 + */ + public static byte[] floatToBytes(float floatValue, ByteOrder byteOrder) { + return intToBytes(Float.floatToIntBits(floatValue), byteOrder); + } + + /** + * byte数组转float
+ * 默认以小端序转换
+ * + * @param bytes byte数组 + * @return float值 + * @since 5.7.18 + */ + public static float bytesToFloat(byte[] bytes) { + return bytesToFloat(bytes, DEFAULT_ORDER); + } + + /** + * byte数组转float
+ * 自定义端序
+ * + * @param bytes byte数组 + * @param byteOrder 端序 + * @return float值 + * @since 5.7.18 + */ + public static float bytesToFloat(byte[] bytes, ByteOrder byteOrder) { + return Float.intBitsToFloat(bytesToInt(bytes, byteOrder)); + } + + /** + * double转byte数组
+ * 默认以小端序转换
+ * + * @param doubleValue double值 + * @return byte数组 + */ + public static byte[] doubleToBytes(double doubleValue) { + return doubleToBytes(doubleValue, DEFAULT_ORDER); + } + + /** + * double转byte数组
+ * 自定义端序
+ * from: https://stackoverflow.com/questions/4485128/how-do-i-convert-long-to-byte-and-back-in-java + * + * @param doubleValue double值 + * @param byteOrder 端序 + * @return byte数组 + */ + public static byte[] doubleToBytes(double doubleValue, ByteOrder byteOrder) { + return longToBytes(Double.doubleToLongBits(doubleValue), byteOrder); + } + + /** + * byte数组转Double
+ * 默认以小端序转换
+ * + * @param bytes byte数组 + * @return long值 + */ + public static double bytesToDouble(byte[] bytes) { + return bytesToDouble(bytes, DEFAULT_ORDER); + } + + /** + * byte数组转double
+ * 自定义端序
+ * + * @param bytes byte数组 + * @param byteOrder 端序 + * @return long值 + */ + public static double bytesToDouble(byte[] bytes, ByteOrder byteOrder) { + return Double.longBitsToDouble(bytesToLong(bytes, byteOrder)); + } + + /** + * 将{@link Number}转换为 + * + * @param number 数字 + * @return bytes + */ + public static byte[] numberToBytes(Number number) { + return numberToBytes(number, DEFAULT_ORDER); + } + + /** + * 将{@link Number}转换为 + * + * @param number 数字 + * @param byteOrder 端序 + * @return bytes + */ + public static byte[] numberToBytes(Number number, ByteOrder byteOrder) { + if(number instanceof Byte){ + return new byte[]{number.byteValue()}; + }else if (number instanceof Double) { + return doubleToBytes((Double) number, byteOrder); + } else if (number instanceof Long) { + return longToBytes((Long) number, byteOrder); + } else if (number instanceof Integer) { + return intToBytes((Integer) number, byteOrder); + } else if (number instanceof Short) { + return shortToBytes((Short) number, byteOrder); + } else if (number instanceof Float) { + return floatToBytes((Float) number, byteOrder); + } else { + return doubleToBytes(number.doubleValue(), byteOrder); + } + } + + /** + * byte数组转换为指定类型数字 + * + * @param 数字类型 + * @param bytes byte数组 + * @param targetClass 目标数字类型 + * @param byteOrder 端序 + * @return 转换后的数字 + * @throws IllegalArgumentException 不支持的数字类型,如用户自定义数字类型 + */ + @SuppressWarnings("unchecked") + public static T bytesToNumber(byte[] bytes, Class targetClass, ByteOrder byteOrder) throws IllegalArgumentException { + Number number; + if (Byte.class == targetClass) { + number = bytes[0]; + } else if (Short.class == targetClass) { + number = bytesToShort(bytes, byteOrder); + } else if (Integer.class == targetClass) { + number = bytesToInt(bytes, byteOrder); + } else if (AtomicInteger.class == targetClass) { + number = new AtomicInteger(bytesToInt(bytes, byteOrder)); + } else if (Long.class == targetClass) { + number = bytesToLong(bytes, byteOrder); + } else if (AtomicLong.class == targetClass) { + number = new AtomicLong(bytesToLong(bytes, byteOrder)); + } else if (LongAdder.class == targetClass) { + final LongAdder longValue = new LongAdder(); + longValue.add(bytesToLong(bytes, byteOrder)); + number = longValue; + } else if (Float.class == targetClass) { + number = bytesToFloat(bytes, byteOrder); + } else if (Double.class == targetClass) { + number = bytesToDouble(bytes, byteOrder); + } else if (DoubleAdder.class == targetClass) { + final DoubleAdder doubleAdder = new DoubleAdder(); + doubleAdder.add(bytesToDouble(bytes, byteOrder)); + number = doubleAdder; + } else if (BigDecimal.class == targetClass) { + number = NumberUtil.toBigDecimal(bytesToDouble(bytes, byteOrder)); + } else if (BigInteger.class == targetClass) { + number = BigInteger.valueOf(bytesToLong(bytes, byteOrder)); + } else if (Number.class == targetClass) { + // 用户没有明确类型具体类型,默认Double + number = bytesToDouble(bytes, byteOrder); + } else { + // 用户自定义类型不支持 + throw new IllegalArgumentException("Unsupported Number type: " + targetClass.getName()); + } + + return (T) number; + } +} diff --git a/src/main/java/cn/hutool/core/util/CharUtil.java b/src/main/java/cn/hutool/core/util/CharUtil.java new file mode 100644 index 0000000..ffdcaba --- /dev/null +++ b/src/main/java/cn/hutool/core/util/CharUtil.java @@ -0,0 +1,391 @@ +package cn.hutool.core.util; + +import cn.hutool.core.text.ASCIIStrCache; +import cn.hutool.core.text.CharPool; + +/** + * 字符工具类
+ * 部分工具来自于Apache Commons系列 + * + * @author looly + * @since 4.0.1 + */ +public class CharUtil implements CharPool { + + /** + * 是否为ASCII字符,ASCII字符位于0~127之间 + * + *
+	 *   CharUtil.isAscii('a')  = true
+	 *   CharUtil.isAscii('A')  = true
+	 *   CharUtil.isAscii('3')  = true
+	 *   CharUtil.isAscii('-')  = true
+	 *   CharUtil.isAscii('\n') = true
+	 *   CharUtil.isAscii('©') = false
+	 * 
+ * + * @param ch 被检查的字符处 + * @return true表示为ASCII字符,ASCII字符位于0~127之间 + */ + public static boolean isAscii(char ch) { + return ch < 128; + } + + /** + * 是否为可见ASCII字符,可见字符位于32~126之间 + * + *
+	 *   CharUtil.isAsciiPrintable('a')  = true
+	 *   CharUtil.isAsciiPrintable('A')  = true
+	 *   CharUtil.isAsciiPrintable('3')  = true
+	 *   CharUtil.isAsciiPrintable('-')  = true
+	 *   CharUtil.isAsciiPrintable('\n') = false
+	 *   CharUtil.isAsciiPrintable('©') = false
+	 * 
+ * + * @param ch 被检查的字符处 + * @return true表示为ASCII可见字符,可见字符位于32~126之间 + */ + public static boolean isAsciiPrintable(char ch) { + return ch >= 32 && ch < 127; + } + + /** + * 是否为ASCII控制符(不可见字符),控制符位于0~31和127 + * + *
+	 *   CharUtil.isAsciiControl('a')  = false
+	 *   CharUtil.isAsciiControl('A')  = false
+	 *   CharUtil.isAsciiControl('3')  = false
+	 *   CharUtil.isAsciiControl('-')  = false
+	 *   CharUtil.isAsciiControl('\n') = true
+	 *   CharUtil.isAsciiControl('©') = false
+	 * 
+ * + * @param ch 被检查的字符 + * @return true表示为控制符,控制符位于0~31和127 + */ + public static boolean isAsciiControl(final char ch) { + return ch < 32 || ch == 127; + } + + /** + * 判断是否为字母(包括大写字母和小写字母)
+ * 字母包括A~Z和a~z + * + *
+	 *   CharUtil.isLetter('a')  = true
+	 *   CharUtil.isLetter('A')  = true
+	 *   CharUtil.isLetter('3')  = false
+	 *   CharUtil.isLetter('-')  = false
+	 *   CharUtil.isLetter('\n') = false
+	 *   CharUtil.isLetter('©') = false
+	 * 
+ * + * @param ch 被检查的字符 + * @return true表示为字母(包括大写字母和小写字母)字母包括A~Z和a~z + */ + public static boolean isLetter(char ch) { + return isLetterUpper(ch) || isLetterLower(ch); + } + + /** + *

+ * 判断是否为大写字母,大写字母包括A~Z + *

+ * + *
+	 *   CharUtil.isLetterUpper('a')  = false
+	 *   CharUtil.isLetterUpper('A')  = true
+	 *   CharUtil.isLetterUpper('3')  = false
+	 *   CharUtil.isLetterUpper('-')  = false
+	 *   CharUtil.isLetterUpper('\n') = false
+	 *   CharUtil.isLetterUpper('©') = false
+	 * 
+ * + * @param ch 被检查的字符 + * @return true表示为大写字母,大写字母包括A~Z + */ + public static boolean isLetterUpper(final char ch) { + return ch >= 'A' && ch <= 'Z'; + } + + /** + *

+ * 检查字符是否为小写字母,小写字母指a~z + *

+ * + *
+	 *   CharUtil.isLetterLower('a')  = true
+	 *   CharUtil.isLetterLower('A')  = false
+	 *   CharUtil.isLetterLower('3')  = false
+	 *   CharUtil.isLetterLower('-')  = false
+	 *   CharUtil.isLetterLower('\n') = false
+	 *   CharUtil.isLetterLower('©') = false
+	 * 
+ * + * @param ch 被检查的字符 + * @return true表示为小写字母,小写字母指a~z + */ + public static boolean isLetterLower(final char ch) { + return ch >= 'a' && ch <= 'z'; + } + + /** + *

+ * 检查是否为数字字符,数字字符指0~9 + *

+ * + *
+	 *   CharUtil.isNumber('a')  = false
+	 *   CharUtil.isNumber('A')  = false
+	 *   CharUtil.isNumber('3')  = true
+	 *   CharUtil.isNumber('-')  = false
+	 *   CharUtil.isNumber('\n') = false
+	 *   CharUtil.isNumber('©') = false
+	 * 
+ * + * @param ch 被检查的字符 + * @return true表示为数字字符,数字字符指0~9 + */ + public static boolean isNumber(char ch) { + return ch >= '0' && ch <= '9'; + } + + /** + * 是否为16进制规范的字符,判断是否为如下字符 + *
+	 * 1. 0~9
+	 * 2. a~f
+	 * 4. A~F
+	 * 
+ * + * @param c 字符 + * @return 是否为16进制规范的字符 + * @since 4.1.5 + */ + public static boolean isHexChar(char c) { + return isNumber(c) || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'); + } + + /** + * 是否为字母或数字,包括A~Z、a~z、0~9 + * + *
+	 *   CharUtil.isLetterOrNumber('a')  = true
+	 *   CharUtil.isLetterOrNumber('A')  = true
+	 *   CharUtil.isLetterOrNumber('3')  = true
+	 *   CharUtil.isLetterOrNumber('-')  = false
+	 *   CharUtil.isLetterOrNumber('\n') = false
+	 *   CharUtil.isLetterOrNumber('©') = false
+	 * 
+ * + * @param ch 被检查的字符 + * @return true表示为字母或数字,包括A~Z、a~z、0~9 + */ + public static boolean isLetterOrNumber(final char ch) { + return isLetter(ch) || isNumber(ch); + } + + /** + * 字符转为字符串
+ * 如果为ASCII字符,使用缓存 + * + * @param c 字符 + * @return 字符串 + * @see ASCIIStrCache#toString(char) + */ + public static String toString(char c) { + return ASCIIStrCache.toString(c); + } + + /** + * 给定类名是否为字符类,字符类包括: + * + *
+	 * Character.class
+	 * char.class
+	 * 
+ * + * @param clazz 被检查的类 + * @return true表示为字符类 + */ + public static boolean isCharClass(Class clazz) { + return clazz == Character.class || clazz == char.class; + } + + /** + * 给定对象对应的类是否为字符类,字符类包括: + * + *
+	 * Character.class
+	 * char.class
+	 * 
+ * + * @param value 被检查的对象 + * @return true表示为字符类 + */ + public static boolean isChar(Object value) { + //noinspection ConstantConditions + return value instanceof Character || value.getClass() == char.class; + } + + /** + * 是否空白符
+ * 空白符包括空格、制表符、全角空格和不间断空格
+ * + * @param c 字符 + * @return 是否空白符 + * @see Character#isWhitespace(int) + * @see Character#isSpaceChar(int) + * @since 4.0.10 + */ + public static boolean isBlankChar(char c) { + return isBlankChar((int) c); + } + + /** + * 是否空白符
+ * 空白符包括空格、制表符、全角空格和不间断空格
+ * + * @param c 字符 + * @return 是否空白符 + * @see Character#isWhitespace(int) + * @see Character#isSpaceChar(int) + * @since 4.0.10 + */ + public static boolean isBlankChar(int c) { + return Character.isWhitespace(c) + || Character.isSpaceChar(c) + || c == '\ufeff' + || c == '\u202a' + || c == '\u0000' + // issue#I5UGSQ,Hangul Filler + || c == '\u3164' + // Braille Pattern Blank + || c == '\u2800' + // MONGOLIAN VOWEL SEPARATOR + || c == '\u180e'; + } + + /** + * 判断是否为emoji表情符
+ * + * @param c 字符 + * @return 是否为emoji + * @since 4.0.8 + */ + public static boolean isEmoji(char c) { + //noinspection ConstantConditions + return !((c == 0x0) || // + (c == 0x9) || // + (c == 0xA) || // + (c == 0xD) || // + ((c >= 0x20) && (c <= 0xD7FF)) || // + ((c >= 0xE000) && (c <= 0xFFFD)) || // + ((c >= 0x100000) && (c <= 0x10FFFF))); + } + + /** + * 是否为Windows或者Linux(Unix)文件分隔符
+ * Windows平台下分隔符为\,Linux(Unix)为/ + * + * @param c 字符 + * @return 是否为Windows或者Linux(Unix)文件分隔符 + * @since 4.1.11 + */ + public static boolean isFileSeparator(char c) { + return SLASH == c || BACKSLASH == c; + } + + /** + * 比较两个字符是否相同 + * + * @param c1 字符1 + * @param c2 字符2 + * @param caseInsensitive 是否忽略大小写 + * @return 是否相同 + * @since 4.0.3 + */ + public static boolean equals(char c1, char c2, boolean caseInsensitive) { + if (caseInsensitive) { + return Character.toLowerCase(c1) == Character.toLowerCase(c2); + } + return c1 == c2; + } + + /** + * 获取字符类型 + * + * @param c 字符 + * @return 字符类型 + * @since 5.2.3 + */ + public static int getType(int c) { + return Character.getType(c); + } + + /** + * 获取给定字符的16进制数值 + * + * @param b 字符 + * @return 16进制字符 + * @since 5.3.1 + */ + public static int digit16(int b) { + return Character.digit(b, 16); + } + + /** + * 将字母、数字转换为带圈的字符: + *
+	 *     '1' -》 '①'
+	 *     'A' -》 'Ⓐ'
+	 *     'a' -》 'ⓐ'
+	 * 
+ *

+ * 获取带圈数字 /封闭式字母数字 ,从1-20,超过1-20报错 + * + * @param c 被转换的字符,如果字符不支持转换,返回原字符 + * @return 转换后的字符 + * @see Unicode_symbols + * @see Alphanumerics + * @since 5.6.2 + */ + public static char toCloseChar(char c) { + int result = c; + if (c >= '1' && c <= '9') { + result = '①' + c - '1'; + } else if (c >= 'A' && c <= 'Z') { + result = 'Ⓐ' + c - 'A'; + } else if (c >= 'a' && c <= 'z') { + result = 'ⓐ' + c - 'a'; + } + return (char) result; + } + + /** + * 将[1-20]数字转换为带圈的字符: + *

+	 *     1 -》 '①'
+	 *     12 -》 '⑫'
+	 *     20 -》 '⑳'
+	 * 
+ * 也称作:封闭式字符,英文:Enclosed Alphanumerics + * + * @param number 被转换的数字 + * @return 转换后的字符 + * @author dazer + * @see 维基百科wikipedia-Unicode_symbols + * @see 维基百科wikipedia-Unicode字符列表 + * @see coolsymbol + * @see 百度百科 特殊字符 + * @since 5.6.2 + */ + public static char toCloseByNumber(int number) { + if (number > 20) { + throw new IllegalArgumentException("Number must be [1-20]"); + } + return (char) ('①' + number - 1); + } +} diff --git a/src/main/java/cn/hutool/core/util/CharsetUtil.java b/src/main/java/cn/hutool/core/util/CharsetUtil.java new file mode 100644 index 0000000..b2fff15 --- /dev/null +++ b/src/main/java/cn/hutool/core/util/CharsetUtil.java @@ -0,0 +1,226 @@ +package cn.hutool.core.util; + +import cn.hutool.core.io.CharsetDetector; +import cn.hutool.core.io.FileUtil; + +import java.io.File; +import java.io.InputStream; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.nio.charset.UnsupportedCharsetException; + +/** + * 字符集工具类 + * + * @author xiaoleilu + */ +public class CharsetUtil { + + /** + * ISO-8859-1 + */ + public static final String ISO_8859_1 = "ISO-8859-1"; + /** + * UTF-8 + */ + public static final String UTF_8 = "UTF-8"; + /** + * GBK + */ + public static final String GBK = "GBK"; + + /** + * ISO-8859-1 + */ + public static final Charset CHARSET_ISO_8859_1 = StandardCharsets.ISO_8859_1; + /** + * UTF-8 + */ + public static final Charset CHARSET_UTF_8 = StandardCharsets.UTF_8; + /** + * GBK + */ + public static final Charset CHARSET_GBK; + + static { + //避免不支持GBK的系统中运行报错 issue#731 + Charset _CHARSET_GBK = null; + try { + _CHARSET_GBK = Charset.forName(GBK); + } catch (UnsupportedCharsetException e) { + //ignore + } + CHARSET_GBK = _CHARSET_GBK; + } + + /** + * 转换为Charset对象 + * + * @param charsetName 字符集,为空则返回默认字符集 + * @return Charset + * @throws UnsupportedCharsetException 编码不支持 + */ + public static Charset charset(String charsetName) throws UnsupportedCharsetException { + return StrUtil.isBlank(charsetName) ? Charset.defaultCharset() : Charset.forName(charsetName); + } + + /** + * 解析字符串编码为Charset对象,解析失败返回系统默认编码 + * + * @param charsetName 字符集,为空则返回默认字符集 + * @return Charset + * @since 5.2.6 + */ + public static Charset parse(String charsetName) { + return parse(charsetName, Charset.defaultCharset()); + } + + /** + * 解析字符串编码为Charset对象,解析失败返回默认编码 + * + * @param charsetName 字符集,为空则返回默认字符集 + * @param defaultCharset 解析失败使用的默认编码 + * @return Charset + * @since 5.2.6 + */ + public static Charset parse(String charsetName, Charset defaultCharset) { + if (StrUtil.isBlank(charsetName)) { + return defaultCharset; + } + + Charset result; + try { + result = Charset.forName(charsetName); + } catch (UnsupportedCharsetException e) { + result = defaultCharset; + } + + return result; + } + + /** + * 转换字符串的字符集编码 + * + * @param source 字符串 + * @param srcCharset 源字符集,默认ISO-8859-1 + * @param destCharset 目标字符集,默认UTF-8 + * @return 转换后的字符集 + */ + public static String convert(String source, String srcCharset, String destCharset) { + return convert(source, Charset.forName(srcCharset), Charset.forName(destCharset)); + } + + /** + * 转换字符串的字符集编码
+ * 当以错误的编码读取为字符串时,打印字符串将出现乱码。
+ * 此方法用于纠正因读取使用编码错误导致的乱码问题。
+ * 例如,在Servlet请求中客户端用GBK编码了请求参数,我们使用UTF-8读取到的是乱码,此时,使用此方法即可还原原编码的内容 + *
+	 * 客户端 -》 GBK编码 -》 Servlet容器 -》 UTF-8解码 -》 乱码
+	 * 乱码 -》 UTF-8编码 -》 GBK解码 -》 正确内容
+	 * 
+ * + * @param source 字符串 + * @param srcCharset 源字符集,默认ISO-8859-1 + * @param destCharset 目标字符集,默认UTF-8 + * @return 转换后的字符集 + */ + public static String convert(String source, Charset srcCharset, Charset destCharset) { + if (null == srcCharset) { + srcCharset = StandardCharsets.ISO_8859_1; + } + + if (null == destCharset) { + destCharset = StandardCharsets.UTF_8; + } + + if (StrUtil.isBlank(source) || srcCharset.equals(destCharset)) { + return source; + } + return new String(source.getBytes(srcCharset), destCharset); + } + + /** + * 转换文件编码
+ * 此方法用于转换文件编码,读取的文件实际编码必须与指定的srcCharset编码一致,否则导致乱码 + * + * @param file 文件 + * @param srcCharset 原文件的编码,必须与文件内容的编码保持一致 + * @param destCharset 转码后的编码 + * @return 被转换编码的文件 + * @since 3.1.0 + */ + public static File convert(File file, Charset srcCharset, Charset destCharset) { + final String str = FileUtil.readString(file, srcCharset); + return FileUtil.writeString(str, file, destCharset); + } + + /** + * 系统字符集编码,如果是Windows,则默认为GBK编码,否则取 {@link CharsetUtil#defaultCharsetName()} + * + * @return 系统字符集编码 + * @see CharsetUtil#defaultCharsetName() + * @since 3.1.2 + */ + public static String systemCharsetName() { + return systemCharset().name(); + } + + /** + * 系统字符集编码,如果是Windows,则默认为GBK编码,否则取 {@link CharsetUtil#defaultCharsetName()} + * + * @return 系统字符集编码 + * @see CharsetUtil#defaultCharsetName() + * @since 3.1.2 + */ + public static Charset systemCharset() { + return FileUtil.isWindows() ? CHARSET_GBK : defaultCharset(); + } + + /** + * 系统默认字符集编码 + * + * @return 系统字符集编码 + */ + public static String defaultCharsetName() { + return defaultCharset().name(); + } + + /** + * 系统默认字符集编码 + * + * @return 系统字符集编码 + */ + public static Charset defaultCharset() { + return Charset.defaultCharset(); + } + + /** + * 探测编码
+ * 注意:此方法会读取流的一部分,然后关闭流,如重复使用流,请使用使用支持reset方法的流 + * + * @param in 流,使用后关闭此流 + * @param charsets 需要测试用的编码,null或空使用默认的编码数组 + * @return 编码 + * @see CharsetDetector#detect(InputStream, Charset...) + * @since 5.7.10 + */ + public static Charset defaultCharset(InputStream in, Charset... charsets) { + return CharsetDetector.detect(in, charsets); + } + + /** + * 探测编码
+ * 注意:此方法会读取流的一部分,然后关闭流,如重复使用流,请使用使用支持reset方法的流 + * + * @param bufferSize 自定义缓存大小,即每次检查的长度 + * @param in 流,使用后关闭此流 + * @param charsets 需要测试用的编码,null或空使用默认的编码数组 + * @return 编码 + * @see CharsetDetector#detect(int, InputStream, Charset...) + * @since 5.7.10 + */ + public static Charset defaultCharset(int bufferSize, InputStream in, Charset... charsets) { + return CharsetDetector.detect(bufferSize, in, charsets); + } +} diff --git a/src/main/java/cn/hutool/core/util/ClassLoaderUtil.java b/src/main/java/cn/hutool/core/util/ClassLoaderUtil.java new file mode 100644 index 0000000..fbf2687 --- /dev/null +++ b/src/main/java/cn/hutool/core/util/ClassLoaderUtil.java @@ -0,0 +1,344 @@ +package cn.hutool.core.util; + +import cn.hutool.core.convert.BasicType; +import cn.hutool.core.exceptions.UtilException; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.lang.JarClassLoader; +import cn.hutool.core.map.SafeConcurrentHashMap; +import cn.hutool.core.text.CharPool; + +import java.io.File; +import java.lang.reflect.Array; +import java.security.AccessController; +import java.security.PrivilegedAction; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** + * {@link ClassLoader}工具类 + * + * @author Looly + * @since 3.0.9 + */ +public class ClassLoaderUtil { + + /** + * 数组类的结尾符: "[]" + */ + private static final String ARRAY_SUFFIX = "[]"; + /** + * 内部数组类名前缀: "[" + */ + private static final String INTERNAL_ARRAY_PREFIX = "["; + /** + * 内部非原始类型类名前缀: "[L" + */ + private static final String NON_PRIMITIVE_ARRAY_PREFIX = "[L"; + /** + * 包名分界符: '.' + */ + private static final char PACKAGE_SEPARATOR = StrUtil.C_DOT; + /** + * 内部类分界符: '$' + */ + private static final char INNER_CLASS_SEPARATOR = '$'; + + /** + * 原始类型名和其class对应表,例如:int =》 int.class + */ + private static final Map> PRIMITIVE_TYPE_NAME_MAP = new SafeConcurrentHashMap<>(32); + + static { + final List> primitiveTypes = new ArrayList<>(32); + // 加入原始类型 + primitiveTypes.addAll(BasicType.PRIMITIVE_WRAPPER_MAP.keySet()); + // 加入原始类型数组类型 + primitiveTypes.add(boolean[].class); + primitiveTypes.add(byte[].class); + primitiveTypes.add(char[].class); + primitiveTypes.add(double[].class); + primitiveTypes.add(float[].class); + primitiveTypes.add(int[].class); + primitiveTypes.add(long[].class); + primitiveTypes.add(short[].class); + primitiveTypes.add(void.class); + for (final Class primitiveType : primitiveTypes) { + PRIMITIVE_TYPE_NAME_MAP.put(primitiveType.getName(), primitiveType); + } + } + + /** + * 获取当前线程的{@link ClassLoader} + * + * @return 当前线程的class loader + * @see Thread#getContextClassLoader() + */ + public static ClassLoader getContextClassLoader() { + if (System.getSecurityManager() == null) { + return Thread.currentThread().getContextClassLoader(); + } else { + // 绕开权限检查 + return AccessController.doPrivileged( + (PrivilegedAction) () -> Thread.currentThread().getContextClassLoader()); + } + } + + /** + * 获取系统{@link ClassLoader} + * + * @return 系统{@link ClassLoader} + * @see ClassLoader#getSystemClassLoader() + * @since 5.7.0 + */ + public static ClassLoader getSystemClassLoader() { + if (System.getSecurityManager() == null) { + return ClassLoader.getSystemClassLoader(); + } else { + // 绕开权限检查 + return AccessController.doPrivileged( + (PrivilegedAction) ClassLoader::getSystemClassLoader); + } + } + + + /** + * 获取{@link ClassLoader}
+ * 获取顺序如下:
+ * + *
+	 * 1、获取当前线程的ContextClassLoader
+	 * 2、获取当前类对应的ClassLoader
+	 * 3、获取系统ClassLoader({@link ClassLoader#getSystemClassLoader()})
+	 * 
+ * + * @return 类加载器 + */ + public static ClassLoader getClassLoader() { + ClassLoader classLoader = getContextClassLoader(); + if (classLoader == null) { + classLoader = ClassLoaderUtil.class.getClassLoader(); + if (null == classLoader) { + classLoader = getSystemClassLoader(); + } + } + return classLoader; + } + + // ----------------------------------------------------------------------------------- loadClass + + /** + * 加载类,通过传入类的字符串,返回其对应的类名,使用默认ClassLoader并初始化类(调用static模块内容和初始化static属性)
+ * 扩展{@link Class#forName(String, boolean, ClassLoader)}方法,支持以下几类类名的加载: + * + *
+	 * 1、原始类型,例如:int
+	 * 2、数组类型,例如:int[]、Long[]、String[]
+	 * 3、内部类,例如:java.lang.Thread.State会被转为java.lang.Thread$State加载
+	 * 
+ * + * @param name 类名 + * @return 类名对应的类 + * @throws UtilException 包装{@link ClassNotFoundException},没有类名对应的类时抛出此异常 + */ + public static Class loadClass(String name) throws UtilException { + return loadClass(name, true); + } + + /** + * 加载类,通过传入类的字符串,返回其对应的类名,使用默认ClassLoader
+ * 扩展{@link Class#forName(String, boolean, ClassLoader)}方法,支持以下几类类名的加载: + * + *
+	 * 1、原始类型,例如:int
+	 * 2、数组类型,例如:int[]、Long[]、String[]
+	 * 3、内部类,例如:java.lang.Thread.State会被转为java.lang.Thread$State加载
+	 * 
+ * + * @param name 类名 + * @param isInitialized 是否初始化类(调用static模块内容和初始化static属性) + * @return 类名对应的类 + * @throws UtilException 包装{@link ClassNotFoundException},没有类名对应的类时抛出此异常 + */ + public static Class loadClass(String name, boolean isInitialized) throws UtilException { + return loadClass(name, null, isInitialized); + } + + /** + * 加载类,通过传入类的字符串,返回其对应的类名
+ * 此方法支持缓存,第一次被加载的类之后会读取缓存中的类
+ * 加载失败的原因可能是此类不存在或其关联引用类不存在
+ * 扩展{@link Class#forName(String, boolean, ClassLoader)}方法,支持以下几类类名的加载: + * + *
+	 * 1、原始类型,例如:int
+	 * 2、数组类型,例如:int[]、Long[]、String[]
+	 * 3、内部类,例如:java.lang.Thread.State会被转为java.lang.Thread$State加载
+	 * 
+ * + * @param name 类名 + * @param classLoader {@link ClassLoader},{@code null} 则使用{@link #getClassLoader()}获取 + * @param isInitialized 是否初始化类(调用static模块内容和初始化static属性) + * @return 类名对应的类 + * @throws UtilException 包装{@link ClassNotFoundException},没有类名对应的类时抛出此异常 + */ + public static Class loadClass(String name, ClassLoader classLoader, boolean isInitialized) throws UtilException { + Assert.notNull(name, "Name must not be null"); + + // 自动将包名中的"/"替换为"." + name = name.replace(CharPool.SLASH, CharPool.DOT); + if(null == classLoader){ + classLoader = getClassLoader(); + } + + // 加载原始类型和缓存中的类 + Class clazz = loadPrimitiveClass(name); + if (clazz == null) { + clazz = doLoadClass(name, classLoader, isInitialized); + } + return clazz; + } + + /** + * 加载原始类型的类。包括原始类型、原始类型数组和void + * + * @param name 原始类型名,比如 int + * @return 原始类型类 + */ + public static Class loadPrimitiveClass(String name) { + Class result = null; + if (StrUtil.isNotBlank(name)) { + name = name.trim(); + if (name.length() <= 8) { + result = PRIMITIVE_TYPE_NAME_MAP.get(name); + } + } + return result; + } + + /** + * 创建新的{@link JarClassLoader},并使用此Classloader加载目录下的class文件和jar文件 + * + * @param jarOrDir jar文件或者包含jar和class文件的目录 + * @return {@link JarClassLoader} + * @since 4.4.2 + */ + public static JarClassLoader getJarClassLoader(File jarOrDir) { + return JarClassLoader.load(jarOrDir); + } + + /** + * 加载外部类 + * + * @param jarOrDir jar文件或者包含jar和class文件的目录 + * @param name 类名 + * @return 类 + * @since 4.4.2 + */ + public static Class loadClass(File jarOrDir, String name) { + try { + return getJarClassLoader(jarOrDir).loadClass(name); + } catch (ClassNotFoundException e) { + throw new UtilException(e); + } + } + + // ----------------------------------------------------------------------------------- isPresent + + /** + * 指定类是否被提供,使用默认ClassLoader
+ * 通过调用{@link #loadClass(String, ClassLoader, boolean)}方法尝试加载指定类名的类,如果加载失败返回false
+ * 加载失败的原因可能是此类不存在或其关联引用类不存在 + * + * @param className 类名 + * @return 是否被提供 + */ + public static boolean isPresent(String className) { + return isPresent(className, null); + } + + /** + * 指定类是否被提供
+ * 通过调用{@link #loadClass(String, ClassLoader, boolean)}方法尝试加载指定类名的类,如果加载失败返回false
+ * 加载失败的原因可能是此类不存在或其关联引用类不存在 + * + * @param className 类名 + * @param classLoader {@link ClassLoader} + * @return 是否被提供 + */ + public static boolean isPresent(String className, ClassLoader classLoader) { + try { + loadClass(className, classLoader, false); + return true; + } catch (Throwable ex) { + return false; + } + } + + // ----------------------------------------------------------------------------------- Private method start + /** + * 加载非原始类类,无缓存 + * @param name 类名 + * @param classLoader {@link ClassLoader} + * @param isInitialized 是否初始化 + * @return 类 + */ + private static Class doLoadClass(String name, ClassLoader classLoader, boolean isInitialized){ + Class clazz; + if (name.endsWith(ARRAY_SUFFIX)) { + // 对象数组"java.lang.String[]"风格 + final String elementClassName = name.substring(0, name.length() - ARRAY_SUFFIX.length()); + final Class elementClass = loadClass(elementClassName, classLoader, isInitialized); + clazz = Array.newInstance(elementClass, 0).getClass(); + } else if (name.startsWith(NON_PRIMITIVE_ARRAY_PREFIX) && name.endsWith(";")) { + // "[Ljava.lang.String;" 风格 + final String elementName = name.substring(NON_PRIMITIVE_ARRAY_PREFIX.length(), name.length() - 1); + final Class elementClass = loadClass(elementName, classLoader, isInitialized); + clazz = Array.newInstance(elementClass, 0).getClass(); + } else if (name.startsWith(INTERNAL_ARRAY_PREFIX)) { + // "[[I" 或 "[[Ljava.lang.String;" 风格 + final String elementName = name.substring(INTERNAL_ARRAY_PREFIX.length()); + final Class elementClass = loadClass(elementName, classLoader, isInitialized); + clazz = Array.newInstance(elementClass, 0).getClass(); + } else { + // 加载普通类 + if (null == classLoader) { + classLoader = getClassLoader(); + } + try { + clazz = Class.forName(name, isInitialized, classLoader); + } catch (ClassNotFoundException ex) { + // 尝试获取内部类,例如java.lang.Thread.State =》java.lang.Thread$State + clazz = tryLoadInnerClass(name, classLoader, isInitialized); + if (null == clazz) { + throw new UtilException(ex); + } + } + } + return clazz; + } + + /** + * 尝试转换并加载内部类,例如java.lang.Thread.State =》java.lang.Thread$State + * + * @param name 类名 + * @param classLoader {@link ClassLoader},{@code null} 则使用系统默认ClassLoader + * @param isInitialized 是否初始化类(调用static模块内容和初始化static属性) + * @return 类名对应的类 + * @since 4.1.20 + */ + private static Class tryLoadInnerClass(String name, ClassLoader classLoader, boolean isInitialized) { + // 尝试获取内部类,例如java.lang.Thread.State =》java.lang.Thread$State + final int lastDotIndex = name.lastIndexOf(PACKAGE_SEPARATOR); + if (lastDotIndex > 0) {// 类与内部类的分隔符不能在第一位,因此>0 + final String innerClassName = name.substring(0, lastDotIndex) + INNER_CLASS_SEPARATOR + name.substring(lastDotIndex + 1); + try { + return Class.forName(innerClassName, isInitialized, classLoader); + } catch (ClassNotFoundException ex2) { + // 尝试获取内部类失败时,忽略之。 + } + } + return null; + } + // ----------------------------------------------------------------------------------- Private method end +} diff --git a/src/main/java/cn/hutool/core/util/ClassUtil.java b/src/main/java/cn/hutool/core/util/ClassUtil.java new file mode 100644 index 0000000..731e90f --- /dev/null +++ b/src/main/java/cn/hutool/core/util/ClassUtil.java @@ -0,0 +1,1135 @@ +package cn.hutool.core.util; + +import cn.hutool.core.bean.NullWrapperBean; +import cn.hutool.core.convert.BasicType; +import cn.hutool.core.exceptions.UtilException; +import cn.hutool.core.io.FileUtil; +import cn.hutool.core.io.IORuntimeException; +import cn.hutool.core.io.resource.ResourceUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.lang.ClassScanner; +import cn.hutool.core.lang.Filter; +import cn.hutool.core.lang.Singleton; + +import java.io.IOException; +import java.lang.annotation.Annotation; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.lang.reflect.Type; +import java.net.URI; +import java.net.URL; +import java.time.temporal.TemporalAccessor; +import java.util.Date; +import java.util.Enumeration; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Set; + +/** + * 类工具类
+ * + * @author xiaoleilu + */ +public class ClassUtil { + + /** + * {@code null}安全的获取对象类型 + * + * @param 对象类型 + * @param obj 对象,如果为{@code null} 返回{@code null} + * @return 对象类型,提供对象如果为{@code null} 返回{@code null} + */ + @SuppressWarnings("unchecked") + public static Class getClass(T obj) { + return ((null == obj) ? null : (Class) obj.getClass()); + } + + /** + * 获得外围类
+ * 返回定义此类或匿名类所在的类,如果类本身是在包中定义的,返回{@code null} + * + * @param clazz 类 + * @return 外围类 + * @since 4.5.7 + */ + public static Class getEnclosingClass(Class clazz) { + return null == clazz ? null : clazz.getEnclosingClass(); + } + + /** + * 是否为顶层类,即定义在包中的类,而非定义在类中的内部类 + * + * @param clazz 类 + * @return 是否为顶层类 + * @since 4.5.7 + */ + public static boolean isTopLevelClass(Class clazz) { + if (null == clazz) { + return false; + } + return null == getEnclosingClass(clazz); + } + + /** + * 获取类名 + * + * @param obj 获取类名对象 + * @param isSimple 是否简单类名,如果为true,返回不带包名的类名 + * @return 类名 + * @since 3.0.7 + */ + public static String getClassName(Object obj, boolean isSimple) { + if (null == obj) { + return null; + } + final Class clazz = obj.getClass(); + return getClassName(clazz, isSimple); + } + + /** + * 获取类名
+ * 类名并不包含“.class”这个扩展名
+ * 例如:ClassUtil这个类
+ * + *
+	 * isSimple为false: "com.xiaoleilu.hutool.util.ClassUtil"
+	 * isSimple为true: "ClassUtil"
+	 * 
+ * + * @param clazz 类 + * @param isSimple 是否简单类名,如果为true,返回不带包名的类名 + * @return 类名 + * @since 3.0.7 + */ + public static String getClassName(Class clazz, boolean isSimple) { + if (null == clazz) { + return null; + } + return isSimple ? clazz.getSimpleName() : clazz.getName(); + } + + /** + * 获取完整类名的短格式如:
+ * cn.hutool.core.util.StrUtil -》c.h.c.u.StrUtil + * + * @param className 类名 + * @return 短格式类名 + * @since 4.1.9 + */ + public static String getShortClassName(String className) { + final List packages = StrUtil.split(className, CharUtil.DOT); + if (null == packages || packages.size() < 2) { + return className; + } + + final int size = packages.size(); + final StringBuilder result = StrUtil.builder(); + result.append(packages.get(0).charAt(0)); + for (int i = 1; i < size - 1; i++) { + result.append(CharUtil.DOT).append(packages.get(i).charAt(0)); + } + result.append(CharUtil.DOT).append(packages.get(size - 1)); + return result.toString(); + } + + /** + * 获得对象数组的类数组 + * + * @param objects 对象数组,如果数组中存在{@code null}元素,则此元素被认为是Object类型 + * @return 类数组 + */ + public static Class[] getClasses(Object... objects) { + Class[] classes = new Class[objects.length]; + Object obj; + for (int i = 0; i < objects.length; i++) { + obj = objects[i]; + if (obj instanceof NullWrapperBean) { + // 自定义null值的参数类型 + classes[i] = ((NullWrapperBean) obj).getWrappedClass(); + } else if (null == obj) { + classes[i] = Object.class; + } else { + classes[i] = obj.getClass(); + } + } + return classes; + } + + /** + * 指定类是否与给定的类名相同 + * + * @param clazz 类 + * @param className 类名,可以是全类名(包含包名),也可以是简单类名(不包含包名) + * @param ignoreCase 是否忽略大小写 + * @return 指定类是否与给定的类名相同 + * @since 3.0.7 + */ + public static boolean equals(Class clazz, String className, boolean ignoreCase) { + if (null == clazz || StrUtil.isBlank(className)) { + return false; + } + if (ignoreCase) { + return className.equalsIgnoreCase(clazz.getName()) || className.equalsIgnoreCase(clazz.getSimpleName()); + } else { + return className.equals(clazz.getName()) || className.equals(clazz.getSimpleName()); + } + } + + // ----------------------------------------------------------------------------------------- Scan classes + + /** + * 扫描指定包路径下所有包含指定注解的类 + * + * @param packageName 包路径 + * @param annotationClass 注解类 + * @return 类集合 + * @see ClassScanner#scanPackageByAnnotation(String, Class) + */ + public static Set> scanPackageByAnnotation(String packageName, final Class annotationClass) { + return ClassScanner.scanPackageByAnnotation(packageName, annotationClass); + } + + /** + * 扫描指定包路径下所有指定类或接口的子类或实现类 + * + * @param packageName 包路径 + * @param superClass 父类或接口 + * @return 类集合 + * @see ClassScanner#scanPackageBySuper(String, Class) + */ + public static Set> scanPackageBySuper(String packageName, final Class superClass) { + return ClassScanner.scanPackageBySuper(packageName, superClass); + } + + /** + * 扫描该包路径下所有class文件 + * + * @return 类集合 + * @see ClassScanner#scanPackage() + */ + public static Set> scanPackage() { + return ClassScanner.scanPackage(); + } + + /** + * 扫描该包路径下所有class文件 + * + * @param packageName 包路径 com | com. | com.abs | com.abs. + * @return 类集合 + * @see ClassScanner#scanPackage(String) + */ + public static Set> scanPackage(String packageName) { + return ClassScanner.scanPackage(packageName); + } + + /** + * 扫描包路径下满足class过滤器条件的所有class文件,
+ * 如果包路径为 com.abs + A.class 但是输入 abs会产生classNotFoundException
+ * 因为className 应该为 com.abs.A 现在却成为abs.A,此工具类对该异常进行忽略处理,有可能是一个不完善的地方,以后需要进行修改
+ * + * @param packageName 包路径 com | com. | com.abs | com.abs. + * @param classFilter class过滤器,过滤掉不需要的class + * @return 类集合 + */ + public static Set> scanPackage(String packageName, Filter> classFilter) { + return ClassScanner.scanPackage(packageName, classFilter); + } + + // ----------------------------------------------------------------------------------------- Method + + /** + * 获得指定类中的Public方法名
+ * 去重重载的方法 + * + * @param clazz 类 + * @return 方法名Set + */ + public static Set getPublicMethodNames(Class clazz) { + return ReflectUtil.getPublicMethodNames(clazz); + } + + /** + * 获得本类及其父类所有Public方法 + * + * @param clazz 查找方法的类 + * @return 过滤后的方法列表 + */ + public static Method[] getPublicMethods(Class clazz) { + return ReflectUtil.getPublicMethods(clazz); + } + + /** + * 获得指定类过滤后的Public方法列表 + * + * @param clazz 查找方法的类 + * @param filter 过滤器 + * @return 过滤后的方法列表 + */ + public static List getPublicMethods(Class clazz, Filter filter) { + return ReflectUtil.getPublicMethods(clazz, filter); + } + + /** + * 获得指定类过滤后的Public方法列表 + * + * @param clazz 查找方法的类 + * @param excludeMethods 不包括的方法 + * @return 过滤后的方法列表 + */ + public static List getPublicMethods(Class clazz, Method... excludeMethods) { + return ReflectUtil.getPublicMethods(clazz, excludeMethods); + } + + /** + * 获得指定类过滤后的Public方法列表 + * + * @param clazz 查找方法的类 + * @param excludeMethodNames 不包括的方法名列表 + * @return 过滤后的方法列表 + */ + public static List getPublicMethods(Class clazz, String... excludeMethodNames) { + return ReflectUtil.getPublicMethods(clazz, excludeMethodNames); + } + + /** + * 查找指定Public方法 如果找不到对应的方法或方法不为public的则返回{@code null} + * + * @param clazz 类 + * @param methodName 方法名 + * @param paramTypes 参数类型 + * @return 方法 + * @throws SecurityException 无权访问抛出异常 + */ + public static Method getPublicMethod(Class clazz, String methodName, Class... paramTypes) throws SecurityException { + return ReflectUtil.getPublicMethod(clazz, methodName, paramTypes); + } + + /** + * 获得指定类中的Public方法名
+ * 去重重载的方法 + * + * @param clazz 类 + * @return 方法名Set + */ + public static Set getDeclaredMethodNames(Class clazz) { + return ReflectUtil.getMethodNames(clazz); + } + + /** + * 获得声明的所有方法,包括本类及其父类和接口的所有方法和Object类的方法 + * + * @param clazz 类 + * @return 方法数组 + */ + public static Method[] getDeclaredMethods(Class clazz) { + return ReflectUtil.getMethods(clazz); + } + + /** + * 查找指定对象中的所有方法(包括非public方法),也包括父对象和Object类的方法 + * + * @param obj 被查找的对象 + * @param methodName 方法名 + * @param args 参数 + * @return 方法 + * @throws SecurityException 无访问权限抛出异常 + */ + public static Method getDeclaredMethodOfObj(Object obj, String methodName, Object... args) throws SecurityException { + return getDeclaredMethod(obj.getClass(), methodName, getClasses(args)); + } + + /** + * 查找指定类中的所有方法(包括非public方法),也包括父类和Object类的方法 找不到方法会返回{@code null} + * + * @param clazz 被查找的类 + * @param methodName 方法名 + * @param parameterTypes 参数类型 + * @return 方法 + * @throws SecurityException 无访问权限抛出异常 + */ + public static Method getDeclaredMethod(Class clazz, String methodName, Class... parameterTypes) throws SecurityException { + return ReflectUtil.getMethod(clazz, methodName, parameterTypes); + } + + // ----------------------------------------------------------------------------------------- Field + + /** + * 查找指定类中的所有字段(包括非public字段), 字段不存在则返回{@code null} + * + * @param clazz 被查找字段的类 + * @param fieldName 字段名 + * @return 字段 + * @throws SecurityException 安全异常 + */ + public static Field getDeclaredField(Class clazz, String fieldName) throws SecurityException { + if (null == clazz || StrUtil.isBlank(fieldName)) { + return null; + } + try { + return clazz.getDeclaredField(fieldName); + } catch (NoSuchFieldException e) { + // e.printStackTrace(); + } + return null; + } + + /** + * 查找指定类中的所有字段(包括非public字段) + * + * @param clazz 被查找字段的类 + * @return 字段 + * @throws SecurityException 安全异常 + */ + public static Field[] getDeclaredFields(Class clazz) throws SecurityException { + if (null == clazz) { + return null; + } + return clazz.getDeclaredFields(); + } + + // ----------------------------------------------------------------------------------------- Classpath + + /** + * 获得ClassPath,不解码路径中的特殊字符(例如空格和中文) + * + * @return ClassPath集合 + */ + public static Set getClassPathResources() { + return getClassPathResources(false); + } + + /** + * 获得ClassPath + * + * @param isDecode 是否解码路径中的特殊字符(例如空格和中文) + * @return ClassPath集合 + * @since 4.0.11 + */ + public static Set getClassPathResources(boolean isDecode) { + return getClassPaths(StrUtil.EMPTY, isDecode); + } + + /** + * 获得ClassPath,不解码路径中的特殊字符(例如空格和中文) + * + * @param packageName 包名称 + * @return ClassPath路径字符串集合 + */ + public static Set getClassPaths(String packageName) { + return getClassPaths(packageName, false); + } + + /** + * 获得ClassPath + * + * @param packageName 包名称 + * @param isDecode 是否解码路径中的特殊字符(例如空格和中文) + * @return ClassPath路径字符串集合 + * @since 4.0.11 + */ + public static Set getClassPaths(String packageName, boolean isDecode) { + String packagePath = packageName.replace(StrUtil.DOT, StrUtil.SLASH); + Enumeration resources; + try { + resources = getClassLoader().getResources(packagePath); + } catch (IOException e) { + throw new UtilException(e, "Loading classPath [{}] error!", packagePath); + } + final Set paths = new HashSet<>(); + String path; + while (resources.hasMoreElements()) { + path = resources.nextElement().getPath(); + paths.add(isDecode ? URLUtil.decode(path, CharsetUtil.systemCharsetName()) : path); + } + return paths; + } + + /** + * 获得ClassPath,将编码后的中文路径解码为原字符
+ * 这个ClassPath路径会文件路径被标准化处理 + * + * @return ClassPath + */ + public static String getClassPath() { + return getClassPath(false); + } + + /** + * 获得ClassPath,这个ClassPath路径会文件路径被标准化处理 + * + * @param isEncoded 是否编码路径中的中文 + * @return ClassPath + * @since 3.2.1 + */ + public static String getClassPath(boolean isEncoded) { + final URL classPathURL = getClassPathURL(); + String url = isEncoded ? classPathURL.getPath() : URLUtil.getDecodedPath(classPathURL); + return FileUtil.normalize(url); + } + + /** + * 获得ClassPath URL + * + * @return ClassPath URL + */ + public static URL getClassPathURL() { + return getResourceURL(StrUtil.EMPTY); + } + + /** + * 获得资源的URL
+ * 路径用/分隔,例如: + * + *
+	 * config/a/db.config
+	 * spring/xml/test.xml
+	 * 
+ * + * @param resource 资源(相对Classpath的路径) + * @return 资源URL + * @see ResourceUtil#getResource(String) + */ + public static URL getResourceURL(String resource) throws IORuntimeException { + return ResourceUtil.getResource(resource); + } + + /** + * 获取指定路径下的资源列表
+ * 路径格式必须为目录格式,用/分隔,例如: + * + *
+	 * config/a
+	 * spring/xml
+	 * 
+ * + * @param resource 资源路径 + * @return 资源列表 + * @see ResourceUtil#getResources(String) + */ + public static List getResources(String resource) { + return ResourceUtil.getResources(resource); + } + + /** + * 获得资源相对路径对应的URL + * + * @param resource 资源相对路径 + * @param baseClass 基准Class,获得的相对路径相对于此Class所在路径,如果为{@code null}则相对ClassPath + * @return {@link URL} + * @see ResourceUtil#getResource(String, Class) + */ + public static URL getResourceUrl(String resource, Class baseClass) { + return ResourceUtil.getResource(resource, baseClass); + } + + /** + * @return 获得Java ClassPath路径,不包括 jre + */ + public static String[] getJavaClassPaths() { + return System.getProperty("java.class.path").split(System.getProperty("path.separator")); + } + + /** + * 获取当前线程的{@link ClassLoader} + * + * @return 当前线程的class loader + * @see ClassLoaderUtil#getClassLoader() + */ + public static ClassLoader getContextClassLoader() { + return ClassLoaderUtil.getContextClassLoader(); + } + + /** + * 获取{@link ClassLoader}
+ * 获取顺序如下:
+ * + *
+	 * 1、获取当前线程的ContextClassLoader
+	 * 2、获取{@link ClassLoaderUtil}类对应的ClassLoader
+	 * 3、获取系统ClassLoader({@link ClassLoader#getSystemClassLoader()})
+	 * 
+ * + * @return 类加载器 + */ + public static ClassLoader getClassLoader() { + return ClassLoaderUtil.getClassLoader(); + } + + /** + * 比较判断types1和types2两组类,如果types1中所有的类都与types2对应位置的类相同,或者是其父类或接口,则返回{@code true} + * + * @param types1 类组1 + * @param types2 类组2 + * @return 是否相同、父类或接口 + */ + public static boolean isAllAssignableFrom(Class[] types1, Class[] types2) { + if (ArrayUtil.isEmpty(types1) && ArrayUtil.isEmpty(types2)) { + return true; + } + if (null == types1 || null == types2) { + // 任何一个为null不相等(之前已判断两个都为null的情况) + return false; + } + if (types1.length != types2.length) { + return false; + } + + Class type1; + Class type2; + for (int i = 0; i < types1.length; i++) { + type1 = types1[i]; + type2 = types2[i]; + if (isBasicType(type1) && isBasicType(type2)) { + // 原始类型和包装类型存在不一致情况 + if (BasicType.unWrap(type1) != BasicType.unWrap(type2)) { + return false; + } + } else if (!type1.isAssignableFrom(type2)) { + return false; + } + } + return true; + } + + /** + * 加载类 + * + * @param 对象类型 + * @param className 类名 + * @param isInitialized 是否初始化 + * @return 类 + */ + @SuppressWarnings("unchecked") + public static Class loadClass(String className, boolean isInitialized) { + return (Class) ClassLoaderUtil.loadClass(className, isInitialized); + } + + /** + * 加载类并初始化 + * + * @param 对象类型 + * @param className 类名 + * @return 类 + */ + public static Class loadClass(String className) { + return loadClass(className, true); + } + + // ---------------------------------------------------------------------------------------------------- Invoke start + + /** + * 执行方法
+ * 可执行Private方法,也可执行static方法
+ * 执行非static方法时,必须满足对象有默认构造方法
+ * 非单例模式,如果是非静态方法,每次创建一个新对象 + * + * @param 对象类型 + * @param classNameWithMethodName 类名和方法名表达式,类名与方法名用{@code .}或{@code #}连接 例如:com.xiaoleilu.hutool.StrUtil.isEmpty 或 com.xiaoleilu.hutool.StrUtil#isEmpty + * @param args 参数,必须严格对应指定方法的参数类型和数量 + * @return 返回结果 + */ + public static T invoke(String classNameWithMethodName, Object[] args) { + return invoke(classNameWithMethodName, false, args); + } + + /** + * 执行方法
+ * 可执行Private方法,也可执行static方法
+ * 执行非static方法时,必须满足对象有默认构造方法
+ * + * @param 对象类型 + * @param classNameWithMethodName 类名和方法名表达式,例如:com.xiaoleilu.hutool.StrUtil#isEmpty或com.xiaoleilu.hutool.StrUtil.isEmpty + * @param isSingleton 是否为单例对象,如果此参数为false,每次执行方法时创建一个新对象 + * @param args 参数,必须严格对应指定方法的参数类型和数量 + * @return 返回结果 + */ + public static T invoke(String classNameWithMethodName, boolean isSingleton, Object... args) { + if (StrUtil.isBlank(classNameWithMethodName)) { + throw new UtilException("Blank classNameDotMethodName!"); + } + + int splitIndex = classNameWithMethodName.lastIndexOf('#'); + if (splitIndex <= 0) { + splitIndex = classNameWithMethodName.lastIndexOf('.'); + } + if (splitIndex <= 0) { + throw new UtilException("Invalid classNameWithMethodName [{}]!", classNameWithMethodName); + } + + final String className = classNameWithMethodName.substring(0, splitIndex); + final String methodName = classNameWithMethodName.substring(splitIndex + 1); + + return invoke(className, methodName, isSingleton, args); + } + + /** + * 执行方法
+ * 可执行Private方法,也可执行static方法
+ * 执行非static方法时,必须满足对象有默认构造方法
+ * 非单例模式,如果是非静态方法,每次创建一个新对象 + * + * @param 对象类型 + * @param className 类名,完整类路径 + * @param methodName 方法名 + * @param args 参数,必须严格对应指定方法的参数类型和数量 + * @return 返回结果 + */ + public static T invoke(String className, String methodName, Object[] args) { + return invoke(className, methodName, false, args); + } + + /** + * 执行方法
+ * 可执行Private方法,也可执行static方法
+ * 执行非static方法时,必须满足对象有默认构造方法
+ * + * @param 对象类型 + * @param className 类名,完整类路径 + * @param methodName 方法名 + * @param isSingleton 是否为单例对象,如果此参数为false,每次执行方法时创建一个新对象 + * @param args 参数,必须严格对应指定方法的参数类型和数量 + * @return 返回结果 + */ + public static T invoke(String className, String methodName, boolean isSingleton, Object... args) { + Class clazz = loadClass(className); + try { + final Method method = getDeclaredMethod(clazz, methodName, getClasses(args)); + if (null == method) { + throw new NoSuchMethodException(StrUtil.format("No such method: [{}]", methodName)); + } + if (isStatic(method)) { + return ReflectUtil.invoke(null, method, args); + } else { + return ReflectUtil.invoke(isSingleton ? Singleton.get(clazz) : clazz.newInstance(), method, args); + } + } catch (Exception e) { + throw new UtilException(e); + } + } + + // ---------------------------------------------------------------------------------------------------- Invoke end + + /** + * 是否为包装类型 + * + * @param clazz 类 + * @return 是否为包装类型 + */ + public static boolean isPrimitiveWrapper(Class clazz) { + if (null == clazz) { + return false; + } + return BasicType.WRAPPER_PRIMITIVE_MAP.containsKey(clazz); + } + + /** + * 是否为基本类型(包括包装类和原始类) + * + * @param clazz 类 + * @return 是否为基本类型 + */ + public static boolean isBasicType(Class clazz) { + if (null == clazz) { + return false; + } + return (clazz.isPrimitive() || isPrimitiveWrapper(clazz)); + } + + /** + * 是否简单值类型或简单值类型的数组
+ * 包括:原始类型,、String、other CharSequence, a Number, a Date, a URI, a URL, a Locale or a Class及其数组 + * + * @param clazz 属性类 + * @return 是否简单值类型或简单值类型的数组 + */ + public static boolean isSimpleTypeOrArray(Class clazz) { + if (null == clazz) { + return false; + } + return isSimpleValueType(clazz) || (clazz.isArray() && isSimpleValueType(clazz.getComponentType())); + } + + /** + * 是否为简单值类型
+ * 包括: + *
+	 *     原始类型
+	 *     String、other CharSequence
+	 *     Number
+	 *     Date
+	 *     URI
+	 *     URL
+	 *     Locale
+	 *     Class
+	 * 
+ * + * @param clazz 类 + * @return 是否为简单值类型 + */ + public static boolean isSimpleValueType(Class clazz) { + return isBasicType(clazz) // + || clazz.isEnum() // + || CharSequence.class.isAssignableFrom(clazz) // + || Number.class.isAssignableFrom(clazz) // + || Date.class.isAssignableFrom(clazz) // + || clazz.equals(URI.class) // + || clazz.equals(URL.class) // + || clazz.equals(Locale.class) // + || clazz.equals(Class.class)// + // jdk8 date object + || TemporalAccessor.class.isAssignableFrom(clazz); // + } + + /** + * 检查目标类是否可以从原类转化
+ * 转化包括:
+ * 1、原类是对象,目标类型是原类型实现的接口
+ * 2、目标类型是原类型的父类
+ * 3、两者是原始类型或者包装类型(相互转换) + * + * @param targetType 目标类型 + * @param sourceType 原类型 + * @return 是否可转化 + */ + public static boolean isAssignable(Class targetType, Class sourceType) { + if (null == targetType || null == sourceType) { + return false; + } + + // 对象类型 + if (targetType.isAssignableFrom(sourceType)) { + return true; + } + + // 基本类型 + if (targetType.isPrimitive()) { + // 原始类型 + Class resolvedPrimitive = BasicType.WRAPPER_PRIMITIVE_MAP.get(sourceType); + return targetType.equals(resolvedPrimitive); + } else { + // 包装类型 + Class resolvedWrapper = BasicType.PRIMITIVE_WRAPPER_MAP.get(sourceType); + return resolvedWrapper != null && targetType.isAssignableFrom(resolvedWrapper); + } + } + + /** + * 指定类是否为Public + * + * @param clazz 类 + * @return 是否为public + */ + public static boolean isPublic(Class clazz) { + if (null == clazz) { + throw new NullPointerException("Class to provided is null."); + } + return Modifier.isPublic(clazz.getModifiers()); + } + + /** + * 指定方法是否为Public + * + * @param method 方法 + * @return 是否为public + */ + public static boolean isPublic(Method method) { + Assert.notNull(method, "Method to provided is null."); + return Modifier.isPublic(method.getModifiers()); + } + + /** + * 指定类是否为非public + * + * @param clazz 类 + * @return 是否为非public + */ + public static boolean isNotPublic(Class clazz) { + return !isPublic(clazz); + } + + /** + * 指定方法是否为非public + * + * @param method 方法 + * @return 是否为非public + */ + public static boolean isNotPublic(Method method) { + return !isPublic(method); + } + + /** + * 是否为静态方法 + * + * @param method 方法 + * @return 是否为静态方法 + */ + public static boolean isStatic(Method method) { + Assert.notNull(method, "Method to provided is null."); + return Modifier.isStatic(method.getModifiers()); + } + + /** + * 设置方法为可访问 + * + * @param method 方法 + * @return 方法 + */ + public static Method setAccessible(Method method) { + if (null != method && !method.isAccessible()) { + method.setAccessible(true); + } + return method; + } + + /** + * 是否为抽象类 + * + * @param clazz 类 + * @return 是否为抽象类 + */ + public static boolean isAbstract(Class clazz) { + return Modifier.isAbstract(clazz.getModifiers()); + } + + /** + * 是否为标准的类
+ * 这个类必须: + * + *
+	 * 1、非接口
+	 * 2、非抽象类
+	 * 3、非Enum枚举
+	 * 4、非数组
+	 * 5、非注解
+	 * 6、非原始类型(int, long等)
+	 * 
+ * + * @param clazz 类 + * @return 是否为标准类 + */ + public static boolean isNormalClass(Class clazz) { + return null != clazz // + && !clazz.isInterface() // + && !isAbstract(clazz) // + && !clazz.isEnum() // + && !clazz.isArray() // + && !clazz.isAnnotation() // + && !clazz.isSynthetic() // + && !clazz.isPrimitive();// + } + + /** + * 判断类是否为枚举类型 + * + * @param clazz 类 + * @return 是否为枚举类型 + * @since 3.2.0 + */ + public static boolean isEnum(Class clazz) { + return null != clazz && clazz.isEnum(); + } + + /** + * 获得给定类的第一个泛型参数 + * + * @param clazz 被检查的类,必须是已经确定泛型类型的类 + * @return {@link Class} + */ + public static Class getTypeArgument(Class clazz) { + return getTypeArgument(clazz, 0); + } + + /** + * 获得给定类的泛型参数 + * + * @param clazz 被检查的类,必须是已经确定泛型类型的类 + * @param index 泛型类型的索引号,即第几个泛型类型 + * @return {@link Class} + */ + public static Class getTypeArgument(Class clazz, int index) { + final Type argumentType = TypeUtil.getTypeArgument(clazz, index); + return TypeUtil.getClass(argumentType); + } + + /** + * 获得给定类所在包的名称
+ * 例如:
+ * com.xiaoleilu.hutool.util.ClassUtil =》 com.xiaoleilu.hutool.util + * + * @param clazz 类 + * @return 包名 + */ + public static String getPackage(Class clazz) { + if (clazz == null) { + return StrUtil.EMPTY; + } + final String className = clazz.getName(); + int packageEndIndex = className.lastIndexOf(StrUtil.DOT); + if (packageEndIndex == -1) { + return StrUtil.EMPTY; + } + return className.substring(0, packageEndIndex); + } + + /** + * 获得给定类所在包的路径
+ * 例如:
+ * com.xiaoleilu.hutool.util.ClassUtil =》 com/xiaoleilu/hutool/util + * + * @param clazz 类 + * @return 包名 + */ + public static String getPackagePath(Class clazz) { + return getPackage(clazz).replace(StrUtil.C_DOT, StrUtil.C_SLASH); + } + + /** + * 获取指定类型分的默认值
+ * 默认值规则为: + * + *
+	 * 1、如果为原始类型,返回0
+	 * 2、非原始类型返回{@code null}
+	 * 
+ * + * @param clazz 类 + * @return 默认值 + * @since 3.0.8 + */ + public static Object getDefaultValue(Class clazz) { + // 原始类型 + if (clazz.isPrimitive()) { + return getPrimitiveDefaultValue(clazz); + } + return null; + } + + /** + * 获取指定原始类型分的默认值
+ * 默认值规则为: + * + *
+	 * 1、如果为原始类型,返回0
+	 * 2、非原始类型返回{@code null}
+	 * 
+ * + * @param clazz 类 + * @return 默认值 + * @since 5.8.0 + */ + public static Object getPrimitiveDefaultValue(Class clazz) { + if (long.class == clazz) { + return 0L; + } else if (int.class == clazz) { + return 0; + } else if (short.class == clazz) { + return (short) 0; + } else if (char.class == clazz) { + return (char) 0; + } else if (byte.class == clazz) { + return (byte) 0; + } else if (double.class == clazz) { + return 0D; + } else if (float.class == clazz) { + return 0f; + } else if (boolean.class == clazz) { + return false; + } + return null; + } + + /** + * 获得默认值列表 + * + * @param classes 值类型 + * @return 默认值列表 + * @since 3.0.9 + */ + public static Object[] getDefaultValues(Class... classes) { + final Object[] values = new Object[classes.length]; + for (int i = 0; i < classes.length; i++) { + values[i] = getDefaultValue(classes[i]); + } + return values; + } + + /** + * 是否为JDK中定义的类或接口,判断依据: + * + *
+	 * 1、以java.、javax.开头的包名
+	 * 2、ClassLoader为null
+	 * 
+ * + * @param clazz 被检查的类 + * @return 是否为JDK中定义的类或接口 + * @since 4.6.5 + */ + public static boolean isJdkClass(Class clazz) { + final Package objectPackage = clazz.getPackage(); + if (null == objectPackage) { + return false; + } + final String objectPackageName = objectPackage.getName(); + return objectPackageName.startsWith("java.") // + || objectPackageName.startsWith("javax.") // + || clazz.getClassLoader() == null; + } + + /** + * 获取class类路径URL, 不管是否在jar包中都会返回文件夹的路径
+ * class在jar包中返回jar所在文件夹,class不在jar中返回文件夹目录
+ * jdk中的类不能使用此方法 + * + * @param clazz 类 + * @return URL + * @since 5.2.4 + */ + public static URL getLocation(Class clazz) { + if (null == clazz) { + return null; + } + return clazz.getProtectionDomain().getCodeSource().getLocation(); + } + + /** + * 获取class类路径, 不管是否在jar包中都会返回文件夹的路径
+ * class在jar包中返回jar所在文件夹,class不在jar中返回文件夹目录
+ * jdk中的类不能使用此方法 + * + * @param clazz 类 + * @return class路径 + * @since 5.2.4 + */ + public static String getLocationPath(Class clazz) { + final URL location = getLocation(clazz); + if (null == location) { + return null; + } + return location.getPath(); + } + + /** + * 是否为抽象类或接口 + * + * @param clazz 类 + * @return 是否为抽象类或接口 + * @since 5.8.2 + */ + public static boolean isAbstractOrInterface(Class clazz) { + return isAbstract(clazz) || isInterface(clazz); + } + + /** + * 是否为接口 + * + * @param clazz 类 + * @return 是否为接口 + * @since 5.8.2 + */ + public static boolean isInterface(Class clazz) { + return clazz.isInterface(); + } +} diff --git a/src/main/java/cn/hutool/core/util/CoordinateUtil.java b/src/main/java/cn/hutool/core/util/CoordinateUtil.java new file mode 100644 index 0000000..99ea98d --- /dev/null +++ b/src/main/java/cn/hutool/core/util/CoordinateUtil.java @@ -0,0 +1,330 @@ +package cn.hutool.core.util; + +import java.io.Serializable; +import java.util.Objects; + +/** + * 坐标系转换相关工具类,主流坐标系包括:
+ *
    + *
  • WGS84坐标系:即地球坐标系,中国外谷歌地图
  • + *
  • GCJ02坐标系:即火星坐标系,高德、腾讯、阿里等使用
  • + *
  • BD09坐标系:即百度坐标系,GCJ02坐标系经加密后的坐标系。百度、搜狗等使用
  • + *
+ *

+ * 坐标转换相关参考: https://tool.lu/coordinate/
+ * 参考:https://github.com/JourWon/coordinate-transform + * + * @author hongzhe.qin(qin462328037at163.com), looly + * @since 5.7.16 + */ +public class CoordinateUtil { + + /** + * 坐标转换参数:(火星坐标系与百度坐标系转换的中间量) + */ + public static final double X_PI = 3.1415926535897932384626433832795 * 3000.0 / 180.0; + + /** + * 坐标转换参数:π + */ + public static final double PI = 3.1415926535897932384626433832795D; + + /** + * 地球半径(Krasovsky 1940) + */ + public static final double RADIUS = 6378245.0D; + + /** + * 修正参数(偏率ee) + */ + public static final double CORRECTION_PARAM = 0.00669342162296594323D; + + /** + * 判断坐标是否在国外
+ * 火星坐标系 (GCJ-02)只对国内有效,国外无需转换 + * + * @param lng 经度 + * @param lat 纬度 + * @return 坐标是否在国外 + */ + public static boolean outOfChina(double lng, double lat) { + return (lng < 72.004 || lng > 137.8347) || (lat < 0.8293 || lat > 55.8271); + } + + //----------------------------------------------------------------------------------- WGS84 + /** + * WGS84 转换为 火星坐标系 (GCJ-02) + * + * @param lng 经度值 + * @param lat 纬度值 + * @return 火星坐标 (GCJ-02) + */ + public static Coordinate wgs84ToGcj02(double lng, double lat) { + return new Coordinate(lng, lat).offset(offset(lng, lat, true)); + } + + /** + * WGS84 坐标转为 百度坐标系 (BD-09) 坐标 + * + * @param lng 经度值 + * @param lat 纬度值 + * @return bd09 坐标 + */ + public static Coordinate wgs84ToBd09(double lng, double lat) { + final Coordinate gcj02 = wgs84ToGcj02(lng, lat); + return gcj02ToBd09(gcj02.lng, gcj02.lat); + } + + //----------------------------------------------------------------------------------- GCJ-02 + /** + * 火星坐标系 (GCJ-02) 转换为 WGS84 + * + * @param lng 经度坐标 + * @param lat 纬度坐标 + * @return WGS84 坐标 + */ + public static Coordinate gcj02ToWgs84(double lng, double lat) { + return new Coordinate(lng, lat).offset(offset(lng, lat, false)); + } + + /** + * 火星坐标系 (GCJ-02) 与百度坐标系 (BD-09) 的转换 + * + * @param lng 经度值 + * @param lat 纬度值 + * @return BD-09 坐标 + */ + public static Coordinate gcj02ToBd09(double lng, double lat) { + double z = Math.sqrt(lng * lng + lat * lat) + 0.00002 * Math.sin(lat * X_PI); + double theta = Math.atan2(lat, lng) + 0.000003 * Math.cos(lng * X_PI); + double bd_lng = z * Math.cos(theta) + 0.0065; + double bd_lat = z * Math.sin(theta) + 0.006; + return new Coordinate(bd_lng, bd_lat); + } + + //----------------------------------------------------------------------------------- BD-09 + /** + * 百度坐标系 (BD-09) 与 火星坐标系 (GCJ-02)的转换 + * 即 百度 转 谷歌、高德 + * + * @param lng 经度值 + * @param lat 纬度值 + * @return GCJ-02 坐标 + */ + public static Coordinate bd09ToGcj02(double lng, double lat) { + double x = lng - 0.0065; + double y = lat - 0.006; + double z = Math.sqrt(x * x + y * y) - 0.00002 * Math.sin(y * X_PI); + double theta = Math.atan2(y, x) - 0.000003 * Math.cos(x * X_PI); + double gg_lng = z * Math.cos(theta); + double gg_lat = z * Math.sin(theta); + return new Coordinate(gg_lng, gg_lat); + } + + /** + * 百度坐标系 (BD-09) 与 WGS84 的转换 + * + * @param lng 经度值 + * @param lat 纬度值 + * @return WGS84坐标 + */ + public static Coordinate bd09toWgs84(double lng, double lat) { + final Coordinate gcj02 = bd09ToGcj02(lng, lat); + return gcj02ToWgs84(gcj02.lng, gcj02.lat); + } + + /** + * WGS84 坐标转为 墨卡托投影 + * + * @param lng 经度值 + * @param lat 纬度值 + * @return 墨卡托投影 + */ + public static Coordinate wgs84ToMercator(double lng, double lat) { + double x = lng * 20037508.342789244 / 180; + double y = Math.log(Math.tan((90 + lat) * Math.PI / 360)) / (Math.PI / 180); + y = y * 20037508.342789244 / 180; + return new Coordinate(x, y); + } + + /** + * 墨卡托投影 转为 WGS84 坐标 + * + * @param mercatorX 墨卡托X坐标 + * @param mercatorY 墨卡托Y坐标 + * @return WGS84 坐标 + */ + public static Coordinate mercatorToWgs84(double mercatorX, double mercatorY) { + double x = mercatorX / 20037508.342789244 * 180; + double y = mercatorY / 20037508.342789244 * 180; + y = 180 / Math.PI * (2 * Math.atan(Math.exp(y * Math.PI / 180)) - Math.PI / 2); + return new Coordinate(x, y); + } + + //----------------------------------------------------------------------------------- Private methods begin + + /** + * WGS84 与 火星坐标系 (GCJ-02)转换的偏移算法(非精确) + * + * @param lng 经度值 + * @param lat 纬度值 + * @param isPlus 是否正向偏移:WGS84转GCJ-02使用正向,否则使用反向 + * @return 偏移坐标 + */ + private static Coordinate offset(double lng, double lat, boolean isPlus) { + double dlng = transLng(lng - 105.0, lat - 35.0); + double dlat = transLat(lng - 105.0, lat - 35.0); + + double magic = Math.sin(lat / 180.0 * PI); + magic = 1 - CORRECTION_PARAM * magic * magic; + final double sqrtMagic = Math.sqrt(magic); + + dlng = (dlng * 180.0) / (RADIUS / sqrtMagic * Math.cos(lat / 180.0 * PI) * PI); + dlat = (dlat * 180.0) / ((RADIUS * (1 - CORRECTION_PARAM)) / (magic * sqrtMagic) * PI); + + if(!isPlus){ + dlng = - dlng; + dlat = - dlat; + } + + return new Coordinate(dlng, dlat); + } + + /** + * 计算经度坐标 + * + * @param lng 经度坐标 + * @param lat 纬度坐标 + * @return ret 计算完成后的 + */ + private static double transLng(double lng, double lat) { + double ret = 300.0 + lng + 2.0 * lat + 0.1 * lng * lng + 0.1 * lng * lat + 0.1 * Math.sqrt(Math.abs(lng)); + ret += (20.0 * Math.sin(6.0 * lng * PI) + 20.0 * Math.sin(2.0 * lng * PI)) * 2.0 / 3.0; + ret += (20.0 * Math.sin(lng * PI) + 40.0 * Math.sin(lng / 3.0 * PI)) * 2.0 / 3.0; + ret += (150.0 * Math.sin(lng / 12.0 * PI) + 300.0 * Math.sin(lng / 30.0 * PI)) * 2.0 / 3.0; + return ret; + } + + /** + * 计算纬度坐标 + * + * @param lng 经度 + * @param lat 纬度 + * @return ret 计算完成后的 + */ + private static double transLat(double lng, double lat) { + double ret = -100.0 + 2.0 * lng + 3.0 * lat + 0.2 * lat * lat + 0.1 * lng * lat + + 0.2 * Math.sqrt(Math.abs(lng)); + ret += (20.0 * Math.sin(6.0 * lng * PI) + 20.0 * Math.sin(2.0 * lng * PI)) * 2.0 / 3.0; + ret += (20.0 * Math.sin(lat * PI) + 40.0 * Math.sin(lat / 3.0 * PI)) * 2.0 / 3.0; + ret += (160.0 * Math.sin(lat / 12.0 * PI) + 320 * Math.sin(lat * PI / 30.0)) * 2.0 / 3.0; + return ret; + } + //----------------------------------------------------------------------------------- Private methods end + + /** + * 坐标经纬度 + * + * @author looly + */ + public static class Coordinate implements Serializable { + private static final long serialVersionUID = 1L; + + /** + * 经度 + */ + private double lng; + /** + * 纬度 + */ + private double lat; + + /** + * 构造 + * + * @param lng 经度 + * @param lat 纬度 + */ + public Coordinate(double lng, double lat) { + this.lng = lng; + this.lat = lat; + } + + /** + * 获取经度 + * + * @return 经度 + */ + public double getLng() { + return lng; + } + + /** + * 设置经度 + * + * @param lng 经度 + * @return this + */ + public Coordinate setLng(double lng) { + this.lng = lng; + return this; + } + + /** + * 获取纬度 + * + * @return 纬度 + */ + public double getLat() { + return lat; + } + + /** + * 设置纬度 + * + * @param lat 纬度 + * @return this + */ + public Coordinate setLat(double lat) { + this.lat = lat; + return this; + } + + /** + * 当前坐标偏移指定坐标 + * + * @param offset 偏移量 + * @return this + */ + public Coordinate offset(Coordinate offset){ + this.lng += offset.lng; + this.lat += offset.lat; + return this; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Coordinate that = (Coordinate) o; + return Double.compare(that.lng, lng) == 0 && Double.compare(that.lat, lat) == 0; + } + + @Override + public int hashCode() { + return Objects.hash(lng, lat); + } + + @Override + public String toString() { + return "Coordinate{" + + "lng=" + lng + + ", lat=" + lat + + '}'; + } + } +} diff --git a/src/main/java/cn/hutool/core/util/CreditCodeUtil.java b/src/main/java/cn/hutool/core/util/CreditCodeUtil.java new file mode 100644 index 0000000..3b8b56d --- /dev/null +++ b/src/main/java/cn/hutool/core/util/CreditCodeUtil.java @@ -0,0 +1,138 @@ +package cn.hutool.core.util; + +import cn.hutool.core.lang.PatternPool; +import cn.hutool.core.map.SafeConcurrentHashMap; + +import java.util.Map; +import java.util.regex.Pattern; + +/** + * 统一社会信用代码(GB32100-2015)工具类
+ * 标准见:https://www.cods.org.cn/c/2020-10-29/12575.html + * + *

+ * 第一部分:登记管理部门代码1位 (数字或大写英文字母)
+ * 第二部分:机构类别代码1位 (数字或大写英文字母)
+ * 第三部分:登记管理机关行政区划码6位 (数字)
+ * 第四部分:主体标识码(组织机构代码)9位 (数字或大写英文字母)
+ * 第五部分:校验码1位 (数字或大写英文字母)
+ * 
+ * + * @author looly + * @since 5.2.4 + */ +public class CreditCodeUtil { + + public static final Pattern CREDIT_CODE_PATTERN = PatternPool.CREDIT_CODE; + + /** + * 加权因子 + */ + private static final int[] WEIGHT = {1, 3, 9, 27, 19, 26, 16, 17, 20, 29, 25, 13, 8, 24, 10, 30, 28}; + /** + * 代码字符集 + */ + private static final char[] BASE_CODE_ARRAY = "0123456789ABCDEFGHJKLMNPQRTUWXY".toCharArray(); + private static final Map CODE_INDEX_MAP; + + static { + CODE_INDEX_MAP = new SafeConcurrentHashMap<>(BASE_CODE_ARRAY.length); + for (int i = 0; i < BASE_CODE_ARRAY.length; i++) { + CODE_INDEX_MAP.put(BASE_CODE_ARRAY[i], i); + } + } + + /** + * 正则校验统一社会信用代码(18位) + * + *
+	 * 第一部分:登记管理部门代码1位 (数字或大写英文字母)
+	 * 第二部分:机构类别代码1位 (数字或大写英文字母)
+	 * 第三部分:登记管理机关行政区划码6位 (数字)
+	 * 第四部分:主体标识码(组织机构代码)9位 (数字或大写英文字母)
+	 * 第五部分:校验码1位 (数字或大写英文字母)
+	 * 
+ * + * @param creditCode 统一社会信用代码 + * @return 校验结果 + */ + public static boolean isCreditCodeSimple(CharSequence creditCode) { + if (StrUtil.isBlank(creditCode)) { + return false; + } + return ReUtil.isMatch(CREDIT_CODE_PATTERN, creditCode); + } + + /** + * 是否是有效的统一社会信用代码 + *
+	 * 第一部分:登记管理部门代码1位 (数字或大写英文字母)
+	 * 第二部分:机构类别代码1位 (数字或大写英文字母)
+	 * 第三部分:登记管理机关行政区划码6位 (数字)
+	 * 第四部分:主体标识码(组织机构代码)9位 (数字或大写英文字母)
+	 * 第五部分:校验码1位 (数字或大写英文字母)
+	 * 
+ * + * @param creditCode 统一社会信用代码 + * @return 校验结果 + */ + public static boolean isCreditCode(CharSequence creditCode) { + if (!isCreditCodeSimple(creditCode)) { + return false; + } + + final int parityBit = getParityBit(creditCode); + if (parityBit < 0) { + return false; + } + + return creditCode.charAt(17) == BASE_CODE_ARRAY[parityBit]; + } + + /** + * 获取一个随机的统一社会信用代码 + * + * @return 统一社会信用代码 + */ + public static String randomCreditCode() { + final StringBuilder buf = new StringBuilder(18); + + + // + for (int i = 0; i < 2; i++) { + int num = RandomUtil.randomInt(BASE_CODE_ARRAY.length - 1); + buf.append(Character.toUpperCase(BASE_CODE_ARRAY[num])); + } + for (int i = 2; i < 8; i++) { + int num = RandomUtil.randomInt(10); + buf.append(BASE_CODE_ARRAY[num]); + } + for (int i = 8; i < 17; i++) { + int num = RandomUtil.randomInt(BASE_CODE_ARRAY.length - 1); + buf.append(BASE_CODE_ARRAY[num]); + } + + final String code = buf.toString(); + return code + BASE_CODE_ARRAY[getParityBit(code)]; + } + + /** + * 获取校验位的值 + * + * @param creditCode 统一社会信息代码 + * @return 获取校验位的值,-1表示获取错误 + */ + private static int getParityBit(CharSequence creditCode) { + int sum = 0; + Integer codeIndex; + for (int i = 0; i < 17; i++) { + codeIndex = CODE_INDEX_MAP.get(creditCode.charAt(i)); + if (null == codeIndex) { + return -1; + } + sum += codeIndex * WEIGHT[i]; + } + final int result = 31 - sum % 31; + return result == 31 ? 0 : result; + } +} diff --git a/src/main/java/cn/hutool/core/util/DesensitizedUtil.java b/src/main/java/cn/hutool/core/util/DesensitizedUtil.java new file mode 100644 index 0000000..2beaa97 --- /dev/null +++ b/src/main/java/cn/hutool/core/util/DesensitizedUtil.java @@ -0,0 +1,360 @@ +package cn.hutool.core.util; + +/** + * 脱敏工具类,支持以下类型信息的脱敏自动处理: + * + *
    + *
  • 用户ID
  • + *
  • 中文名
  • + *
  • 身份证
  • + *
  • 座机号
  • + *
  • 手机号
  • + *
  • 地址
  • + *
  • 电子邮件
  • + *
  • 密码
  • + *
  • 车牌
  • + *
  • 银行卡号
  • + *
+ * + * @author dazer and neusoft and qiaomu + * @since 5.6.2 + */ +public class DesensitizedUtil { + + /** + * 支持的脱敏类型枚举 + * + * @author dazer and neusoft and qiaomu + */ + public enum DesensitizedType { + /** + * 用户id + */ + USER_ID, + /** + * 中文名 + */ + CHINESE_NAME, + /** + * 身份证号 + */ + ID_CARD, + /** + * 座机号 + */ + FIXED_PHONE, + /** + * 手机号 + */ + MOBILE_PHONE, + /** + * 地址 + */ + ADDRESS, + /** + * 电子邮件 + */ + EMAIL, + /** + * 密码 + */ + PASSWORD, + /** + * 中国大陆车牌,包含普通车辆、新能源车辆 + */ + CAR_LICENSE, + /** + * 银行卡 + */ + BANK_CARD, + /** + * IPv4地址 + */ + IPV4, + /** + * IPv6地址 + */ + IPV6, + /** + * 定义了一个first_mask的规则,只显示第一个字符。 + */ + FIRST_MASK + } + + /** + * 脱敏,使用默认的脱敏策略 + *
+	 * DesensitizedUtil.desensitized("100", DesensitizedUtil.DesensitizedType.USER_ID)) =  "0"
+	 * DesensitizedUtil.desensitized("段正淳", DesensitizedUtil.DesensitizedType.CHINESE_NAME)) = "段**"
+	 * DesensitizedUtil.desensitized("51343620000320711X", DesensitizedUtil.DesensitizedType.ID_CARD)) = "5***************1X"
+	 * DesensitizedUtil.desensitized("09157518479", DesensitizedUtil.DesensitizedType.FIXED_PHONE)) = "0915*****79"
+	 * DesensitizedUtil.desensitized("18049531999", DesensitizedUtil.DesensitizedType.MOBILE_PHONE)) = "180****1999"
+	 * DesensitizedUtil.desensitized("北京市海淀区马连洼街道289号", DesensitizedUtil.DesensitizedType.ADDRESS)) = "北京市海淀区马********"
+	 * DesensitizedUtil.desensitized("duandazhi-jack@gmail.com.cn", DesensitizedUtil.DesensitizedType.EMAIL)) = "d*************@gmail.com.cn"
+	 * DesensitizedUtil.desensitized("1234567890", DesensitizedUtil.DesensitizedType.PASSWORD)) = "**********"
+	 * DesensitizedUtil.desensitized("苏D40000", DesensitizedUtil.DesensitizedType.CAR_LICENSE)) = "苏D4***0"
+	 * DesensitizedUtil.desensitized("11011111222233333256", DesensitizedUtil.DesensitizedType.BANK_CARD)) = "1101 **** **** **** 3256"
+	 * DesensitizedUtil.desensitized("192.168.1.1", DesensitizedUtil.DesensitizedType.IPV4)) = "192.*.*.*"
+	 * 
+ * + * @param str 字符串 + * @param desensitizedType 脱敏类型;可以脱敏:用户id、中文名、身份证号、座机号、手机号、地址、电子邮件、密码 + * @return 脱敏之后的字符串 + * @author dazer and neusoft and qiaomu + * @since 5.6.2 + */ + public static String desensitized(CharSequence str, DesensitizedType desensitizedType) { + if (StrUtil.isBlank(str)) { + return StrUtil.EMPTY; + } + String newStr = String.valueOf(str); + switch (desensitizedType) { + case USER_ID: + newStr = String.valueOf(userId()); + break; + case CHINESE_NAME: + newStr = chineseName(String.valueOf(str)); + break; + case ID_CARD: + newStr = idCardNum(String.valueOf(str), 1, 2); + break; + case FIXED_PHONE: + newStr = fixedPhone(String.valueOf(str)); + break; + case MOBILE_PHONE: + newStr = mobilePhone(String.valueOf(str)); + break; + case ADDRESS: + newStr = address(String.valueOf(str), 8); + break; + case EMAIL: + newStr = email(String.valueOf(str)); + break; + case PASSWORD: + newStr = password(String.valueOf(str)); + break; + case CAR_LICENSE: + newStr = carLicense(String.valueOf(str)); + break; + case BANK_CARD: + newStr = bankCard(String.valueOf(str)); + break; + case IPV4: + newStr = ipv4(String.valueOf(str)); + break; + case IPV6: + newStr = ipv6(String.valueOf(str)); + break; + case FIRST_MASK: + newStr = firstMask(String.valueOf(str)); + break; + default: + } + return newStr; + } + + /** + * 【用户id】不对外提供userId + * + * @return 脱敏后的主键 + */ + public static Long userId() { + return 0L; + } + + /** + * 定义了一个first_mask的规则,只显示第一个字符。
+ * 脱敏前:123456789;脱敏后:1********。 + * + * @param str 字符串 + * @return 脱敏后的字符串 + */ + public static String firstMask(String str) { + if (StrUtil.isBlank(str)) { + return StrUtil.EMPTY; + } + return StrUtil.hide(str, 1, str.length()); + } + + /** + * 【中文姓名】只显示第一个汉字,其他隐藏为2个星号,比如:李** + * + * @param fullName 姓名 + * @return 脱敏后的姓名 + */ + public static String chineseName(String fullName) { + return firstMask(fullName); + } + + /** + * 【身份证号】前1位 和后2位 + * + * @param idCardNum 身份证 + * @param front 保留:前面的front位数;从1开始 + * @param end 保留:后面的end位数;从1开始 + * @return 脱敏后的身份证 + */ + public static String idCardNum(String idCardNum, int front, int end) { + //身份证不能为空 + if (StrUtil.isBlank(idCardNum)) { + return StrUtil.EMPTY; + } + //需要截取的长度不能大于身份证号长度 + if ((front + end) > idCardNum.length()) { + return StrUtil.EMPTY; + } + //需要截取的不能小于0 + if (front < 0 || end < 0) { + return StrUtil.EMPTY; + } + return StrUtil.hide(idCardNum, front, idCardNum.length() - end); + } + + /** + * 【固定电话 前四位,后两位 + * + * @param num 固定电话 + * @return 脱敏后的固定电话; + */ + public static String fixedPhone(String num) { + if (StrUtil.isBlank(num)) { + return StrUtil.EMPTY; + } + return StrUtil.hide(num, 4, num.length() - 2); + } + + /** + * 【手机号码】前三位,后4位,其他隐藏,比如135****2210 + * + * @param num 移动电话; + * @return 脱敏后的移动电话; + */ + public static String mobilePhone(String num) { + if (StrUtil.isBlank(num)) { + return StrUtil.EMPTY; + } + return StrUtil.hide(num, 3, num.length() - 4); + } + + /** + * 【地址】只显示到地区,不显示详细地址,比如:北京市海淀区**** + * + * @param address 家庭住址 + * @param sensitiveSize 敏感信息长度 + * @return 脱敏后的家庭地址 + */ + public static String address(String address, int sensitiveSize) { + if (StrUtil.isBlank(address)) { + return StrUtil.EMPTY; + } + int length = address.length(); + return StrUtil.hide(address, length - sensitiveSize, length); + } + + /** + * 【电子邮箱】邮箱前缀仅显示第一个字母,前缀其他隐藏,用星号代替,@及后面的地址显示,比如:d**@126.com + * + * @param email 邮箱 + * @return 脱敏后的邮箱 + */ + public static String email(String email) { + if (StrUtil.isBlank(email)) { + return StrUtil.EMPTY; + } + int index = StrUtil.indexOf(email, '@'); + if (index <= 1) { + return email; + } + return StrUtil.hide(email, 1, index); + } + + /** + * 【密码】密码的全部字符都用*代替,比如:****** + * + * @param password 密码 + * @return 脱敏后的密码 + */ + public static String password(String password) { + if (StrUtil.isBlank(password)) { + return StrUtil.EMPTY; + } + return StrUtil.repeat('*', password.length()); + } + + /** + * 【中国车牌】车牌中间用*代替 + * eg1:null -》 "" + * eg1:"" -》 "" + * eg3:苏D40000 -》 苏D4***0 + * eg4:陕A12345D -》 陕A1****D + * eg5:京A123 -》 京A123 如果是错误的车牌,不处理 + * + * @param carLicense 完整的车牌号 + * @return 脱敏后的车牌 + */ + public static String carLicense(String carLicense) { + if (StrUtil.isBlank(carLicense)) { + return StrUtil.EMPTY; + } + // 普通车牌 + if (carLicense.length() == 7) { + carLicense = StrUtil.hide(carLicense, 3, 6); + } else if (carLicense.length() == 8) { + // 新能源车牌 + carLicense = StrUtil.hide(carLicense, 3, 7); + } + return carLicense; + } + + /** + * 银行卡号脱敏 + * eg: 1101 **** **** **** 3256 + * + * @param bankCardNo 银行卡号 + * @return 脱敏之后的银行卡号 + * @since 5.6.3 + */ + public static String bankCard(String bankCardNo) { + if (StrUtil.isBlank(bankCardNo)) { + return bankCardNo; + } + bankCardNo = StrUtil.trim(bankCardNo); + if (bankCardNo.length() < 9) { + return bankCardNo; + } + + final int length = bankCardNo.length(); + final int midLength = length - 8; + final StringBuilder buf = new StringBuilder(); + + buf.append(bankCardNo, 0, 4); + for (int i = 0; i < midLength; ++i) { + if (i % 4 == 0) { + buf.append(CharUtil.SPACE); + } + buf.append('*'); + } + buf.append(CharUtil.SPACE).append(bankCardNo, length - 4, length); + return buf.toString(); + } + + /** + * IPv4脱敏,如:脱敏前:192.0.2.1;脱敏后:192.*.*.*。 + * + * @param ipv4 IPv4地址 + * @return 脱敏后的地址 + */ + public static String ipv4(String ipv4) { + return StrUtil.subBefore(ipv4, '.', false) + ".*.*.*"; + } + + /** + * IPv4脱敏,如:脱敏前:2001:0db8:86a3:08d3:1319:8a2e:0370:7344;脱敏后:2001:*:*:*:*:*:*:* + * + * @param ipv6 IPv4地址 + * @return 脱敏后的地址 + */ + public static String ipv6(String ipv6) { + return StrUtil.subBefore(ipv6, ':', false) + ":*:*:*:*:*:*:*"; + } +} diff --git a/src/main/java/cn/hutool/core/util/EnumUtil.java b/src/main/java/cn/hutool/core/util/EnumUtil.java new file mode 100644 index 0000000..f645bc6 --- /dev/null +++ b/src/main/java/cn/hutool/core/util/EnumUtil.java @@ -0,0 +1,371 @@ +package cn.hutool.core.util; + +import cn.hutool.core.lang.Assert; +import cn.hutool.core.lang.func.Func1; +import cn.hutool.core.lang.func.LambdaUtil; +import cn.hutool.core.map.MapUtil; + +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.function.Predicate; + +/** + * 枚举工具类 + * + * @author looly + * @since 3.3.0 + */ +public class EnumUtil { + + /** + * 指定类是否为Enum类 + * + * @param clazz 类 + * @return 是否为Enum类 + */ + public static boolean isEnum(Class clazz) { + Assert.notNull(clazz); + return clazz.isEnum(); + } + + /** + * 指定类是否为Enum类 + * + * @param obj 类 + * @return 是否为Enum类 + */ + public static boolean isEnum(Object obj) { + Assert.notNull(obj); + return obj.getClass().isEnum(); + } + + /** + * Enum对象转String,调用{@link Enum#name()} 方法 + * + * @param e Enum + * @return name值 + * @since 4.1.13 + */ + public static String toString(Enum e) { + return null != e ? e.name() : null; + } + + /** + * 获取给定位置的枚举值 + * + * @param 枚举类型泛型 + * @param enumClass 枚举类 + * @param index 枚举索引 + * @return 枚举值,null表示无此对应枚举 + * @since 5.1.6 + */ + public static > E getEnumAt(Class enumClass, int index) { + final E[] enumConstants = enumClass.getEnumConstants(); + return index >= 0 && index < enumConstants.length ? enumConstants[index] : null; + } + + /** + * 字符串转枚举,调用{@link Enum#valueOf(Class, String)} + * + * @param 枚举类型泛型 + * @param enumClass 枚举类 + * @param value 值 + * @return 枚举值 + * @since 4.1.13 + */ + public static > E fromString(Class enumClass, String value) { + return Enum.valueOf(enumClass, value); + } + + /** + * 字符串转枚举,调用{@link Enum#valueOf(Class, String)}
+ * 如果无枚举值,返回默认值 + * + * @param 枚举类型泛型 + * @param enumClass 枚举类 + * @param value 值 + * @param defaultValue 无对应枚举值返回的默认值 + * @return 枚举值 + * @since 4.5.18 + */ + public static > E fromString(Class enumClass, String value, E defaultValue) { + return ObjectUtil.defaultIfNull(fromStringQuietly(enumClass, value), defaultValue); + } + + /** + * 字符串转枚举,调用{@link Enum#valueOf(Class, String)},转换失败返回{@code null} 而非报错 + * + * @param 枚举类型泛型 + * @param enumClass 枚举类 + * @param value 值 + * @return 枚举值 + * @since 4.5.18 + */ + public static > E fromStringQuietly(Class enumClass, String value) { + if (null == enumClass || StrUtil.isBlank(value)) { + return null; + } + + try { + return fromString(enumClass, value); + } catch (IllegalArgumentException e) { + return null; + } + } + + /** + * 模糊匹配转换为枚举,给定一个值,匹配枚举中定义的所有字段名(包括name属性),一旦匹配到返回这个枚举对象,否则返回null + * + * @param 枚举类型 + * @param enumClass 枚举类 + * @param value 值 + * @return 匹配到的枚举对象,未匹配到返回null + */ + @SuppressWarnings("unchecked") + public static > E likeValueOf(Class enumClass, Object value) { + if (value instanceof CharSequence) { + value = value.toString().trim(); + } + + final Field[] fields = ReflectUtil.getFields(enumClass); + final Enum[] enums = enumClass.getEnumConstants(); + String fieldName; + for (Field field : fields) { + fieldName = field.getName(); + if (field.getType().isEnum() || "ENUM$VALUES".equals(fieldName) || "ordinal".equals(fieldName)) { + // 跳过一些特殊字段 + continue; + } + for (Enum enumObj : enums) { + if (ObjectUtil.equal(value, ReflectUtil.getFieldValue(enumObj, field))) { + return (E) enumObj; + } + } + } + return null; + } + + /** + * 枚举类中所有枚举对象的name列表 + * + * @param clazz 枚举类 + * @return name列表 + */ + public static List getNames(Class> clazz) { + final Enum[] enums = clazz.getEnumConstants(); + if (null == enums) { + return null; + } + final List list = new ArrayList<>(enums.length); + for (Enum e : enums) { + list.add(e.name()); + } + return list; + } + + /** + * 获得枚举类中各枚举对象下指定字段的值 + * + * @param clazz 枚举类 + * @param fieldName 字段名,最终调用getXXX方法 + * @return 字段值列表 + */ + public static List getFieldValues(Class> clazz, String fieldName) { + final Enum[] enums = clazz.getEnumConstants(); + if (null == enums) { + return null; + } + final List list = new ArrayList<>(enums.length); + for (Enum e : enums) { + list.add(ReflectUtil.getFieldValue(e, fieldName)); + } + return list; + } + + /** + * 获得枚举类中所有的字段名
+ * 除用户自定义的字段名,也包括“name”字段,例如: + * + *
+	 *   EnumUtil.getFieldNames(Color.class) == ["name", "index"]
+	 * 
+ * + * @param clazz 枚举类 + * @return 字段名列表 + * @since 4.1.20 + */ + public static List getFieldNames(Class> clazz) { + final List names = new ArrayList<>(); + final Field[] fields = ReflectUtil.getFields(clazz); + String name; + for (Field field : fields) { + name = field.getName(); + if (field.getType().isEnum() || name.contains("$VALUES") || "ordinal".equals(name)) { + continue; + } + if (!names.contains(name)) { + names.add(name); + } + } + return names; + } + + /** + * 通过 某字段对应值 获取 枚举,获取不到时为 {@code null} + * + * @param enumClass 枚举类 + * @param predicate 条件 + * @param 枚举类型 + * @return 对应枚举 ,获取不到时为 {@code null} + * @since 5.8.0 + */ + public static > E getBy(Class enumClass, Predicate predicate) { + return Arrays.stream(enumClass.getEnumConstants()) + .filter(predicate).findFirst().orElse(null); + } + + /** + * 通过 某字段对应值 获取 枚举,获取不到时为 {@code null} + * + * @param condition 条件字段 + * @param value 条件字段值 + * @param 枚举类型 + * @param 字段类型 + * @return 对应枚举 ,获取不到时为 {@code null} + */ + public static , C> E getBy(Func1 condition, C value) { + Class implClass = LambdaUtil.getRealClass(condition); + if (Enum.class.equals(implClass)) { + implClass = LambdaUtil.getRealClass(condition); + } + return Arrays.stream(implClass.getEnumConstants()).filter(e -> condition.callWithRuntimeException(e).equals(value)).findAny().orElse(null); + } + + /** + * 通过 某字段对应值 获取 枚举,获取不到时为 {@code defaultEnum} + * + * @param 枚举类型 + * @param 字段类型 + * @param condition 条件字段 + * @param value 条件字段值 + * @param defaultEnum 条件找不到则返回结果使用这个 + * @return 对应枚举 ,获取不到时为 {@code null} + * @since 5.8.8 + */ + public static , C> E getBy(Func1 condition, C value, E defaultEnum) { + return ObjectUtil.defaultIfNull(getBy(condition, value), defaultEnum); + } + + /** + * 通过 某字段对应值 获取 枚举中另一字段值,获取不到时为 {@code null} + * + * @param field 你想要获取的字段 + * @param condition 条件字段 + * @param value 条件字段值 + * @param 枚举类型 + * @param 想要获取的字段类型 + * @param 条件字段类型 + * @return 对应枚举中另一字段值 ,获取不到时为 {@code null} + * @since 5.8.0 + */ + public static , F, C> F getFieldBy(Func1 field, + Function condition, C value) { + Class implClass = LambdaUtil.getRealClass(field); + if (Enum.class.equals(implClass)) { + implClass = LambdaUtil.getRealClass(field); + } + return Arrays.stream(implClass.getEnumConstants()) + // 过滤 + .filter(e -> condition.apply(e).equals(value)) + // 获取第一个并转换为结果 + .findFirst().map(field::callWithRuntimeException).orElse(null); + } + + /** + * 获取枚举字符串值和枚举对象的Map对应,使用LinkedHashMap保证有序
+ * 结果中键为枚举名,值为枚举对象 + * + * @param 枚举类型 + * @param enumClass 枚举类 + * @return 枚举字符串值和枚举对象的Map对应,使用LinkedHashMap保证有序 + * @since 4.0.2 + */ + public static > LinkedHashMap getEnumMap(final Class enumClass) { + final LinkedHashMap map = new LinkedHashMap<>(); + for (final E e : enumClass.getEnumConstants()) { + map.put(e.name(), e); + } + return map; + } + + /** + * 获得枚举名对应指定字段值的Map
+ * 键为枚举名,值为字段值 + * + * @param clazz 枚举类 + * @param fieldName 字段名,最终调用getXXX方法 + * @return 枚举名对应指定字段值的Map + */ + public static Map getNameFieldMap(Class> clazz, String fieldName) { + final Enum[] enums = clazz.getEnumConstants(); + if (null == enums) { + return null; + } + final Map map = MapUtil.newHashMap(enums.length, true); + for (Enum e : enums) { + map.put(e.name(), ReflectUtil.getFieldValue(e, fieldName)); + } + return map; + } + + /** + * 判断某个值是存在枚举中 + * + * @param 枚举类型 + * @param enumClass 枚举类 + * @param val 需要查找的值 + * @return 是否存在 + */ + public static > boolean contains(final Class enumClass, String val) { + return EnumUtil.getEnumMap(enumClass).containsKey(val); + } + + /** + * 判断某个值是不存在枚举中 + * + * @param 枚举类型 + * @param enumClass 枚举类 + * @param val 需要查找的值 + * @return 是否不存在 + */ + public static > boolean notContains(final Class enumClass, String val) { + return !contains(enumClass, val); + } + + /** + * 忽略大小检查某个枚举值是否匹配指定值 + * + * @param e 枚举值 + * @param val 需要判断的值 + * @return 是非匹配 + */ + public static boolean equalsIgnoreCase(final Enum e, String val) { + return StrUtil.equalsIgnoreCase(toString(e), val); + } + + /** + * 检查某个枚举值是否匹配指定值 + * + * @param e 枚举值 + * @param val 需要判断的值 + * @return 是非匹配 + */ + public static boolean equals(final Enum e, String val) { + return StrUtil.equals(toString(e), val); + } +} diff --git a/src/main/java/cn/hutool/core/util/EscapeUtil.java b/src/main/java/cn/hutool/core/util/EscapeUtil.java new file mode 100644 index 0000000..4bf9175 --- /dev/null +++ b/src/main/java/cn/hutool/core/util/EscapeUtil.java @@ -0,0 +1,199 @@ +package cn.hutool.core.util; + +import cn.hutool.core.lang.Filter; +import cn.hutool.core.text.escape.Html4Escape; +import cn.hutool.core.text.escape.Html4Unescape; +import cn.hutool.core.text.escape.XmlEscape; +import cn.hutool.core.text.escape.XmlUnescape; + +/** + * 转义和反转义工具类Escape / Unescape
+ * escape采用ISO Latin字符集对指定的字符串进行编码。
+ * 所有的空格符、标点符号、特殊字符以及其他非ASCII字符都将被转化成%xx格式的字符编码(xx等于该字符在字符集表里面的编码的16进制数字)。 + * TODO 6.x迁移到core.text.escape包下 + * + * @author xiaoleilu + */ +public class EscapeUtil { + + /** + * 不转义的符号编码 + */ + private static final String NOT_ESCAPE_CHARS = "*@-_+./"; + private static final Filter JS_ESCAPE_FILTER = c -> !( + Character.isDigit(c) + || Character.isLowerCase(c) + || Character.isUpperCase(c) + || StrUtil.contains(NOT_ESCAPE_CHARS, c) + ); + + /** + * 转义XML中的特殊字符
+ *
+	 * 	 & (ampersand) 替换为 &amp;
+	 * 	 < (less than) 替换为 &lt;
+	 * 	 > (greater than) 替换为 &gt;
+	 * 	 " (double quote) 替换为 &quot;
+	 * 	 ' (single quote / apostrophe) 替换为 &apos;
+	 * 
+ * + * @param xml XML文本 + * @return 转义后的文本 + * @since 5.7.2 + */ + public static String escapeXml(CharSequence xml) { + XmlEscape escape = new XmlEscape(); + return escape.replace(xml).toString(); + } + + /** + * 反转义XML中的特殊字符 + * + * @param xml XML文本 + * @return 转义后的文本 + * @since 5.7.2 + */ + public static String unescapeXml(CharSequence xml) { + XmlUnescape unescape = new XmlUnescape(); + return unescape.replace(xml).toString(); + } + + /** + * 转义HTML4中的特殊字符 + * + * @param html HTML文本 + * @return 转义后的文本 + * @since 4.1.5 + */ + public static String escapeHtml4(CharSequence html) { + Html4Escape escape = new Html4Escape(); + return escape.replace(html).toString(); + } + + /** + * 反转义HTML4中的特殊字符 + * + * @param html HTML文本 + * @return 转义后的文本 + * @since 4.1.5 + */ + public static String unescapeHtml4(CharSequence html) { + Html4Unescape unescape = new Html4Unescape(); + return unescape.replace(html).toString(); + } + + /** + * Escape编码(Unicode)(等同于JS的escape()方法)
+ * 该方法不会对 ASCII 字母和数字进行编码,也不会对下面这些 ASCII 标点符号进行编码: * @ - _ + . /
+ * 其他所有的字符都会被转义序列替换。 + * + * @param content 被转义的内容 + * @return 编码后的字符串 + */ + public static String escape(CharSequence content) { + return escape(content, JS_ESCAPE_FILTER); + } + + /** + * Escape编码(Unicode)
+ * 该方法不会对 ASCII 字母和数字进行编码。其他所有的字符都会被转义序列替换。 + * + * @param content 被转义的内容 + * @return 编码后的字符串 + */ + public static String escapeAll(CharSequence content) { + return escape(content, c -> true); + } + + /** + * Escape编码(Unicode)
+ * 该方法不会对 ASCII 字母和数字进行编码。其他所有的字符都会被转义序列替换。 + * + * @param content 被转义的内容 + * @param filter 编码过滤器,对于过滤器中accept为false的字符不做编码 + * @return 编码后的字符串 + */ + public static String escape(CharSequence content, Filter filter) { + if (StrUtil.isEmpty(content)) { + return StrUtil.str(content); + } + + final StringBuilder tmp = new StringBuilder(content.length() * 6); + char c; + for (int i = 0; i < content.length(); i++) { + c = content.charAt(i); + if (!filter.accept(c)) { + tmp.append(c); + } else if (c < 256) { + tmp.append("%"); + if (c < 16) { + tmp.append("0"); + } + tmp.append(Integer.toString(c, 16)); + } else { + tmp.append("%u"); + if(c <= 0xfff){ + // issue#I49JU8@Gitee + tmp.append("0"); + } + tmp.append(Integer.toString(c, 16)); + } + } + return tmp.toString(); + } + + /** + * Escape解码 + * + * @param content 被转义的内容 + * @return 解码后的字符串 + */ + public static String unescape(String content) { + if (StrUtil.isBlank(content)) { + return content; + } + + StringBuilder tmp = new StringBuilder(content.length()); + int lastPos = 0; + int pos; + char ch; + while (lastPos < content.length()) { + pos = content.indexOf("%", lastPos); + if (pos == lastPos) { + if (content.charAt(pos + 1) == 'u') { + ch = (char) Integer.parseInt(content.substring(pos + 2, pos + 6), 16); + tmp.append(ch); + lastPos = pos + 6; + } else { + ch = (char) Integer.parseInt(content.substring(pos + 1, pos + 3), 16); + tmp.append(ch); + lastPos = pos + 3; + } + } else { + if (pos == -1) { + tmp.append(content.substring(lastPos)); + lastPos = content.length(); + } else { + tmp.append(content, lastPos, pos); + lastPos = pos; + } + } + } + return tmp.toString(); + } + + /** + * 安全的unescape文本,当文本不是被escape的时候,返回原文。 + * + * @param content 内容 + * @return 解码后的字符串,如果解码失败返回原字符串 + */ + public static String safeUnescape(String content) { + try { + return unescape(content); + } catch (Exception e) { + // Ignore Exception + } + return content; + } +} diff --git a/src/main/java/cn/hutool/core/util/HashUtil.java b/src/main/java/cn/hutool/core/util/HashUtil.java new file mode 100644 index 0000000..e504560 --- /dev/null +++ b/src/main/java/cn/hutool/core/util/HashUtil.java @@ -0,0 +1,629 @@ +package cn.hutool.core.util; + +import cn.hutool.core.lang.hash.CityHash; +import cn.hutool.core.lang.hash.MetroHash; +import cn.hutool.core.lang.hash.MurmurHash; +import cn.hutool.core.lang.hash.Number128; + +/** + * Hash算法大全
+ * 推荐使用FNV1算法 + * + * @author Goodzzp, Looly + */ +public class HashUtil { + + /** + * 加法hash + * + * @param key 字符串 + * @param prime 一个质数 + * @return hash结果 + */ + public static int additiveHash(String key, int prime) { + int hash, i; + for (hash = key.length(), i = 0; i < key.length(); i++) { + hash += key.charAt(i); + } + return hash % prime; + } + + /** + * 旋转hash + * + * @param key 输入字符串 + * @param prime 质数 + * @return hash值 + */ + public static int rotatingHash(String key, int prime) { + int hash, i; + for (hash = key.length(), i = 0; i < key.length(); ++i) { + hash = (hash << 4) ^ (hash >> 28) ^ key.charAt(i); + } + + // 使用:hash = (hash ^ (hash>>10) ^ (hash>>20)) & mask; + // 替代:hash %= prime; + // return (hash ^ (hash>>10) ^ (hash>>20)); + return hash % prime; + } + + /** + * 一次一个hash + * + * @param key 输入字符串 + * @return 输出hash值 + */ + public static int oneByOneHash(String key) { + int hash, i; + for (hash = 0, i = 0; i < key.length(); ++i) { + hash += key.charAt(i); + hash += (hash << 10); + hash ^= (hash >> 6); + } + hash += (hash << 3); + hash ^= (hash >> 11); + hash += (hash << 15); + // return (hash & M_MASK); + return hash; + } + + /** + * Bernstein's hash + * + * @param key 输入字节数组 + * @return 结果hash + */ + public static int bernstein(String key) { + int hash = 0; + int i; + for (i = 0; i < key.length(); ++i) { + hash = 33 * hash + key.charAt(i); + } + return hash; + } + + /** + * Universal Hashing + * + * @param key 字节数组 + * @param mask 掩码 + * @param tab tab + * @return hash值 + */ + public static int universal(char[] key, int mask, int[] tab) { + int hash = key.length, i, len = key.length; + for (i = 0; i < (len << 3); i += 8) { + char k = key[i >> 3]; + if ((k & 0x01) == 0) { + hash ^= tab[i]; + } + if ((k & 0x02) == 0) { + hash ^= tab[i + 1]; + } + if ((k & 0x04) == 0) { + hash ^= tab[i + 2]; + } + if ((k & 0x08) == 0) { + hash ^= tab[i + 3]; + } + if ((k & 0x10) == 0) { + hash ^= tab[i + 4]; + } + if ((k & 0x20) == 0) { + hash ^= tab[i + 5]; + } + if ((k & 0x40) == 0) { + hash ^= tab[i + 6]; + } + if ((k & 0x80) == 0) { + hash ^= tab[i + 7]; + } + } + return (hash & mask); + } + + /** + * Zobrist Hashing + * + * @param key 字节数组 + * @param mask 掩码 + * @param tab tab + * @return hash值 + */ + public static int zobrist(char[] key, int mask, int[][] tab) { + int hash, i; + for (hash = key.length, i = 0; i < key.length; ++i) { + hash ^= tab[i][key[i]]; + } + return (hash & mask); + } + + /** + * 改进的32位FNV算法1 + * + * @param data 数组 + * @return hash结果 + */ + public static int fnvHash(byte[] data) { + final int p = 16777619; + int hash = (int) 2166136261L; + for (byte b : data) { + hash = (hash ^ b) * p; + } + hash += hash << 13; + hash ^= hash >> 7; + hash += hash << 3; + hash ^= hash >> 17; + hash += hash << 5; + return Math.abs(hash); + } + + /** + * 改进的32位FNV算法1 + * + * @param data 字符串 + * @return hash结果 + */ + public static int fnvHash(String data) { + final int p = 16777619; + int hash = (int) 2166136261L; + for (int i = 0; i < data.length(); i++) { + hash = (hash ^ data.charAt(i)) * p; + } + hash += hash << 13; + hash ^= hash >> 7; + hash += hash << 3; + hash ^= hash >> 17; + hash += hash << 5; + return Math.abs(hash); + } + + /** + * Thomas Wang的算法,整数hash + * + * @param key 整数 + * @return hash值 + */ + public static int intHash(int key) { + key += ~(key << 15); + key ^= (key >>> 10); + key += (key << 3); + key ^= (key >>> 6); + key += ~(key << 11); + key ^= (key >>> 16); + return key; + } + + /** + * RS算法hash + * + * @param str 字符串 + * @return hash值 + */ + public static int rsHash(String str) { + int b = 378551; + int a = 63689; + int hash = 0; + + for (int i = 0; i < str.length(); i++) { + hash = hash * a + str.charAt(i); + a = a * b; + } + + return hash & 0x7FFFFFFF; + } + + /** + * JS算法 + * + * @param str 字符串 + * @return hash值 + */ + public static int jsHash(String str) { + int hash = 1315423911; + + for (int i = 0; i < str.length(); i++) { + hash ^= ((hash << 5) + str.charAt(i) + (hash >> 2)); + } + + return Math.abs(hash) & 0x7FFFFFFF; + } + + /** + * PJW算法 + * + * @param str 字符串 + * @return hash值 + */ + public static int pjwHash(String str) { + int bitsInUnsignedInt = 32; + int threeQuarters = (bitsInUnsignedInt * 3) / 4; + int oneEighth = bitsInUnsignedInt / 8; + int highBits = 0xFFFFFFFF << (bitsInUnsignedInt - oneEighth); + int hash = 0; + int test; + + for (int i = 0; i < str.length(); i++) { + hash = (hash << oneEighth) + str.charAt(i); + + if ((test = hash & highBits) != 0) { + hash = ((hash ^ (test >> threeQuarters)) & (~highBits)); + } + } + + return hash & 0x7FFFFFFF; + } + + /** + * ELF算法 + * + * @param str 字符串 + * @return hash值 + */ + public static int elfHash(String str) { + int hash = 0; + int x; + + for (int i = 0; i < str.length(); i++) { + hash = (hash << 4) + str.charAt(i); + if ((x = (int) (hash & 0xF0000000L)) != 0) { + hash ^= (x >> 24); + hash &= ~x; + } + } + + return hash & 0x7FFFFFFF; + } + + /** + * BKDR算法 + * + * @param str 字符串 + * @return hash值 + */ + public static int bkdrHash(String str) { + int seed = 131; // 31 131 1313 13131 131313 etc.. + int hash = 0; + + for (int i = 0; i < str.length(); i++) { + hash = (hash * seed) + str.charAt(i); + } + + return hash & 0x7FFFFFFF; + } + + /** + * SDBM算法 + * + * @param str 字符串 + * @return hash值 + */ + public static int sdbmHash(String str) { + int hash = 0; + + for (int i = 0; i < str.length(); i++) { + hash = str.charAt(i) + (hash << 6) + (hash << 16) - hash; + } + + return hash & 0x7FFFFFFF; + } + + /** + * DJB算法 + * + * @param str 字符串 + * @return hash值 + */ + public static int djbHash(String str) { + int hash = 5381; + + for (int i = 0; i < str.length(); i++) { + hash = ((hash << 5) + hash) + str.charAt(i); + } + + return hash & 0x7FFFFFFF; + } + + /** + * DEK算法 + * + * @param str 字符串 + * @return hash值 + */ + public static int dekHash(String str) { + int hash = str.length(); + + for (int i = 0; i < str.length(); i++) { + hash = ((hash << 5) ^ (hash >> 27)) ^ str.charAt(i); + } + + return hash & 0x7FFFFFFF; + } + + /** + * AP算法 + * + * @param str 字符串 + * @return hash值 + */ + public static int apHash(String str) { + int hash = 0; + + for (int i = 0; i < str.length(); i++) { + hash ^= ((i & 1) == 0) ? ((hash << 7) ^ str.charAt(i) ^ (hash >> 3)) : (~((hash << 11) ^ str.charAt(i) ^ (hash >> 5))); + } + + // return (hash & 0x7FFFFFFF); + return hash; + } + + /** + * TianL Hash算法 + * + * @param str 字符串 + * @return Hash值 + */ + public static long tianlHash(String str) { + long hash; + + int iLength = str.length(); + if (iLength == 0) { + return 0; + } + + if (iLength <= 256) { + hash = 16777216L * (iLength - 1); + } else { + hash = 4278190080L; + } + + int i; + + char ucChar; + + if (iLength <= 96) { + for (i = 1; i <= iLength; i++) { + ucChar = str.charAt(i - 1); + if (ucChar <= 'Z' && ucChar >= 'A') { + ucChar = (char) (ucChar + 32); + } + hash += (3L * i * ucChar * ucChar + 5L * i * ucChar + 7L * i + 11 * ucChar) % 16777216; + } + } else { + for (i = 1; i <= 96; i++) { + ucChar = str.charAt(i + iLength - 96 - 1); + if (ucChar <= 'Z' && ucChar >= 'A') { + ucChar = (char) (ucChar + 32); + } + hash += (3L * i * ucChar * ucChar + 5L * i * ucChar + 7L * i + 11 * ucChar) % 16777216; + } + } + if (hash < 0) { + hash *= -1; + } + return hash; + } + + /** + * JAVA自己带的算法 + * + * @param str 字符串 + * @return hash值 + */ + public static int javaDefaultHash(String str) { + int h = 0; + int off = 0; + int len = str.length(); + for (int i = 0; i < len; i++) { + h = 31 * h + str.charAt(off++); + } + return h; + } + + /** + * 混合hash算法,输出64位的值 + * + * @param str 字符串 + * @return hash值 + */ + public static long mixHash(String str) { + long hash = str.hashCode(); + hash <<= 32; + hash |= fnvHash(str); + return hash; + } + + /** + * 根据对象的内存地址生成相应的Hash值 + * + * @param obj 对象 + * @return hash值 + * @since 4.2.2 + */ + public static int identityHashCode(Object obj) { + return System.identityHashCode(obj); + } + + /** + * MurmurHash算法32-bit实现 + * + * @param data 数据 + * @return hash值 + * @since 4.3.3 + */ + public static int murmur32(byte[] data) { + return MurmurHash.hash32(data); + } + + /** + * MurmurHash算法64-bit实现 + * + * @param data 数据 + * @return hash值 + * @since 4.3.3 + */ + public static long murmur64(byte[] data) { + return MurmurHash.hash64(data); + } + + /** + * MurmurHash算法128-bit实现 + * + * @param data 数据 + * @return hash值 + * @since 4.3.3 + */ + public static long[] murmur128(byte[] data) { + return MurmurHash.hash128(data); + } + + /** + * CityHash算法32-bit实现 + * + * @param data 数据 + * @return hash值 + * @since 5.2.5 + */ + public static int cityHash32(byte[] data) { + return CityHash.hash32(data); + } + + /** + * CityHash算法64-bit实现,种子1使用默认的CityHash#k2 + * + * @param data 数据 + * @param seed 种子2 + * @return hash值 + * @since 5.2.5 + */ + public static long cityHash64(byte[] data, long seed) { + return CityHash.hash64(data, seed); + } + + /** + * CityHash算法64-bit实现,种子1使用默认的CityHash#k2 + * + * @param data 数据 + * @param seed0 种子1 + * @param seed1 种子2 + * @return hash值 + * @since 5.2.5 + */ + public static long cityHash64(byte[] data, long seed0, long seed1) { + return CityHash.hash64(data, seed0, seed1); + } + + /** + * CityHash算法64-bit实现 + * + * @param data 数据 + * @return hash值 + * @since 5.2.5 + */ + public static long cityHash64(byte[] data) { + return CityHash.hash64(data); + } + + /** + * CityHash算法128-bit实现 + * + * @param data 数据 + * @return hash值 + * @since 5.2.5 + */ + public static long[] cityHash128(byte[] data) { + return CityHash.hash128(data).getLongArray(); + } + + /** + * CityHash算法128-bit实现 + * + * @param data 数据 + * @param seed 种子 + * @return hash值,long[0]:低位,long[1]:高位 + * @since 5.2.5 + */ + public static long[] cityHash128(byte[] data, Number128 seed) { + return CityHash.hash128(data, seed).getLongArray(); + } + + /** + * MetroHash 算法64-bit实现 + * + * @param data 数据 + * @param seed 种子 + * @return hash值 + */ + public static long metroHash64(byte[] data, long seed) { + return MetroHash.hash64(data, seed); + } + + /** + * MetroHash 算法64-bit实现 + * + * @param data 数据 + * @return hash值 + */ + public static long metroHash64(byte[] data) { + return MetroHash.hash64(data); + } + + /** + * MetroHash 算法128-bit实现 + * + * @param data 数据 + * @param seed 种子 + * @return hash值,long[0]:低位,long[1]:高位 + */ + public static long[] metroHash128(byte[] data, long seed) { + return MetroHash.hash128(data, seed).getLongArray(); + } + + /** + * MetroHash 算法128-bit实现 + * + * @param data 数据 + * @return hash值,long[0]:低位,long[1]:高位 + */ + public static long[] metroHash128(byte[] data) { + return MetroHash.hash128(data).getLongArray(); + } + + /** + * HF Hash算法 + * + * @param data 字符串 + * @return hash结果 + * @since 5.8.0 + */ + public static long hfHash(String data) { + int length = data.length(); + long hash = 0; + + for (int i = 0; i < length; i++) { + hash += (long) data.charAt(i) * 3 * i; + } + + if (hash < 0) { + hash = -hash; + } + + return hash; + } + + /** + * HFIP Hash算法 + * + * @param data 字符串 + * @return hash结果 + * @since 5.8.0 + */ + public static long hfIpHash(String data) { + int length = data.length(); + long hash = 0; + for (int i = 0; i < length; i++) { + hash += data.charAt(i % 4) ^ data.charAt(i); + } + return hash; + } +} diff --git a/src/main/java/cn/hutool/core/util/HexUtil.java b/src/main/java/cn/hutool/core/util/HexUtil.java new file mode 100644 index 0000000..7fdf7f2 --- /dev/null +++ b/src/main/java/cn/hutool/core/util/HexUtil.java @@ -0,0 +1,326 @@ +package cn.hutool.core.util; + +import cn.hutool.core.codec.Base16Codec; +import cn.hutool.core.exceptions.UtilException; + +import java.math.BigInteger; +import java.nio.charset.Charset; + +/** + * 十六进制(简写为hex或下标16)在数学中是一种逢16进1的进位制,一般用数字0到9和字母A到F表示(其中:A~F即10~15)。
+ * 例如十进制数57,在二进制写作111001,在16进制写作39。
+ * 像java,c这样的语言为了区分十六进制和十进制数值,会在十六进制数的前面加上 0x,比如0x20是十进制的32,而不是十进制的20
+ *

+ * 参考:https://my.oschina.net/xinxingegeya/blog/287476 + * + * @author Looly + */ +public class HexUtil { + + /** + * 判断给定字符串是否为16进制数
+ * 如果是,需要使用对应数字类型对象的{@code decode}方法解码
+ * 例如:{@code Integer.decode}方法解码int类型的16进制数字 + * + * @param value 值 + * @return 是否为16进制 + */ + public static boolean isHexNumber(String value) { + if(StrUtil.startWith(value, '-')){ + // issue#2875 + return false; + } + int index = 0; + if (value.startsWith("0x", index) || value.startsWith("0X", index)) { + index += 2; + } else if (value.startsWith("#", index)) { + index ++; + } + try { + new BigInteger(value.substring(index), 16); + } catch (final NumberFormatException e) { + return false; + } + return true; + } + + // ---------------------------------------------------------------------------------------------------- encode + + /** + * 将字节数组转换为十六进制字符数组 + * + * @param data byte[] + * @return 十六进制char[] + */ + public static char[] encodeHex(byte[] data) { + return encodeHex(data, true); + } + + /** + * 将字节数组转换为十六进制字符数组 + * + * @param str 字符串 + * @param charset 编码 + * @return 十六进制char[] + */ + public static char[] encodeHex(String str, Charset charset) { + return encodeHex(StrUtil.bytes(str, charset), true); + } + + /** + * 将字节数组转换为十六进制字符数组 + * + * @param data byte[] + * @param toLowerCase {@code true} 传换成小写格式 , {@code false} 传换成大写格式 + * @return 十六进制char[] + */ + public static char[] encodeHex(byte[] data, boolean toLowerCase) { + return (toLowerCase ? Base16Codec.CODEC_LOWER : Base16Codec.CODEC_UPPER).encode(data); + } + + /** + * 将字节数组转换为十六进制字符串 + * + * @param data byte[] + * @return 十六进制String + */ + public static String encodeHexStr(byte[] data) { + return encodeHexStr(data, true); + } + + /** + * 将字符串转换为十六进制字符串,结果为小写 + * + * @param data 需要被编码的字符串 + * @param charset 编码 + * @return 十六进制String + */ + public static String encodeHexStr(String data, Charset charset) { + return encodeHexStr(StrUtil.bytes(data, charset), true); + } + + /** + * 将字符串转换为十六进制字符串,结果为小写,默认编码是UTF-8 + * + * @param data 被编码的字符串 + * @return 十六进制String + */ + public static String encodeHexStr(String data) { + return encodeHexStr(data, CharsetUtil.CHARSET_UTF_8); + } + + /** + * 将字节数组转换为十六进制字符串 + * + * @param data byte[] + * @param toLowerCase {@code true} 传换成小写格式 , {@code false} 传换成大写格式 + * @return 十六进制String + */ + public static String encodeHexStr(byte[] data, boolean toLowerCase) { + return new String(encodeHex(data, toLowerCase)); + } + + // ---------------------------------------------------------------------------------------------------- decode + + /** + * 将十六进制字符数组转换为字符串,默认编码UTF-8 + * + * @param hexStr 十六进制String + * @return 字符串 + */ + public static String decodeHexStr(String hexStr) { + return decodeHexStr(hexStr, CharsetUtil.CHARSET_UTF_8); + } + + /** + * 将十六进制字符数组转换为字符串 + * + * @param hexStr 十六进制String + * @param charset 编码 + * @return 字符串 + */ + public static String decodeHexStr(String hexStr, Charset charset) { + if (StrUtil.isEmpty(hexStr)) { + return hexStr; + } + return StrUtil.str(decodeHex(hexStr), charset); + } + + /** + * 将十六进制字符数组转换为字符串 + * + * @param hexData 十六进制char[] + * @param charset 编码 + * @return 字符串 + */ + public static String decodeHexStr(char[] hexData, Charset charset) { + return StrUtil.str(decodeHex(hexData), charset); + } + + /** + * 将十六进制字符串解码为byte[] + * + * @param hexStr 十六进制String + * @return byte[] + */ + public static byte[] decodeHex(String hexStr) { + return decodeHex((CharSequence) hexStr); + } + + /** + * 将十六进制字符数组转换为字节数组 + * + * @param hexData 十六进制char[] + * @return byte[] + * @throws RuntimeException 如果源十六进制字符数组是一个奇怪的长度,将抛出运行时异常 + */ + public static byte[] decodeHex(char[] hexData) { + return decodeHex(String.valueOf(hexData)); + } + + /** + * 将十六进制字符数组转换为字节数组 + * + * @param hexData 十六进制字符串 + * @return byte[] + * @throws UtilException 如果源十六进制字符数组是一个奇怪的长度,将抛出运行时异常 + * @since 5.6.6 + */ + public static byte[] decodeHex(CharSequence hexData) { + return Base16Codec.CODEC_LOWER.decode(hexData); + } + + // ---------------------------------------------------------------------------------------- Color + + + /** + * 将指定int值转换为Unicode字符串形式,常用于特殊字符(例如汉字)转Unicode形式
+ * 转换的字符串如果u后不足4位,则前面用0填充,例如: + * + *

+	 * '你' =》\u4f60
+	 * 
+ * + * @param value int值,也可以是char + * @return Unicode表现形式 + */ + public static String toUnicodeHex(int value) { + final StringBuilder builder = new StringBuilder(6); + + builder.append("\\u"); + String hex = toHex(value); + int len = hex.length(); + if (len < 4) { + builder.append("0000", 0, 4 - len);// 不足4位补0 + } + builder.append(hex); + + return builder.toString(); + } + + /** + * 将指定char值转换为Unicode字符串形式,常用于特殊字符(例如汉字)转Unicode形式
+ * 转换的字符串如果u后不足4位,则前面用0填充,例如: + * + *
+	 * '你' =》'\u4f60'
+	 * 
+ * + * @param ch char值 + * @return Unicode表现形式 + * @since 4.0.1 + */ + public static String toUnicodeHex(char ch) { + return Base16Codec.CODEC_LOWER.toUnicodeHex(ch); + } + + /** + * 转为16进制字符串 + * + * @param value int值 + * @return 16进制字符串 + * @since 4.4.1 + */ + public static String toHex(int value) { + return Integer.toHexString(value); + } + + /** + * 16进制字符串转为int + * + * @param value 16进制字符串 + * @return 16进制字符串int值 + * @since 5.7.4 + */ + public static int hexToInt(String value) { + return Integer.parseInt(value, 16); + } + + /** + * 转为16进制字符串 + * + * @param value int值 + * @return 16进制字符串 + * @since 4.4.1 + */ + public static String toHex(long value) { + return Long.toHexString(value); + } + + /** + * 16进制字符串转为long + * + * @param value 16进制字符串 + * @return long值 + * @since 5.7.4 + */ + public static long hexToLong(String value) { + return Long.parseLong(value, 16); + } + + /** + * 将byte值转为16进制并添加到{@link StringBuilder}中 + * + * @param builder {@link StringBuilder} + * @param b byte + * @param toLowerCase 是否使用小写 + * @since 4.4.1 + */ + public static void appendHex(StringBuilder builder, byte b, boolean toLowerCase) { + (toLowerCase ? Base16Codec.CODEC_LOWER : Base16Codec.CODEC_UPPER).appendHex(builder, b); + } + + /** + * Hex(16进制)字符串转为BigInteger + * + * @param hexStr Hex(16进制字符串) + * @return {@link BigInteger} + * @since 5.2.0 + */ + public static BigInteger toBigInteger(String hexStr) { + if (null == hexStr) { + return null; + } + return new BigInteger(hexStr, 16); + } + + /** + * 格式化Hex字符串,结果为每2位加一个空格,类似于: + *
+	 *     e8 8c 67 03 80 cb 22 00 95 26 8f
+	 * 
+ * + * @param hexStr Hex字符串 + * @return 格式化后的字符串 + */ + public static String format(String hexStr) { + final int length = hexStr.length(); + final StringBuilder builder = StrUtil.builder(length + length / 2); + builder.append(hexStr.charAt(0)).append(hexStr.charAt(1)); + for (int i = 2; i < length - 1; i += 2) { + builder.append(CharUtil.SPACE).append(hexStr.charAt(i)).append(hexStr.charAt(i + 1)); + } + return builder.toString(); + } + +} diff --git a/src/main/java/cn/hutool/core/util/IdcardUtil.java b/src/main/java/cn/hutool/core/util/IdcardUtil.java new file mode 100644 index 0000000..85df23d --- /dev/null +++ b/src/main/java/cn/hutool/core/util/IdcardUtil.java @@ -0,0 +1,809 @@ +package cn.hutool.core.util; + +import cn.hutool.core.date.DatePattern; +import cn.hutool.core.date.DateTime; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.lang.PatternPool; +import cn.hutool.core.lang.Validator; + +import java.io.Serializable; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +/** + * 身份证相关工具类
+ * see https://www.oschina.net/code/snippet_1611_2881 + * + *

+ * 本工具并没有对行政区划代码做校验,如有需求,请参阅(2018年10月): + * http://www.mca.gov.cn/article/sj/xzqh/2018/201804-12/20181011221630.html + *

+ * + * @author Looly + * @since 3.0.4 + */ +public class IdcardUtil { + + /** + * 中国公民身份证号码最小长度。 + */ + private static final int CHINA_ID_MIN_LENGTH = 15; + /** + * 中国公民身份证号码最大长度。 + */ + private static final int CHINA_ID_MAX_LENGTH = 18; + /** + * 每位加权因子 + */ + private static final int[] POWER = {7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2}; + /** + * 省市代码表 + */ + private static final Map CITY_CODES = new HashMap<>(); + /** + * 台湾身份首字母对应数字 + */ + private static final Map TW_FIRST_CODE = new HashMap<>(); + + static { + CITY_CODES.put("11", "北京"); + CITY_CODES.put("12", "天津"); + CITY_CODES.put("13", "河北"); + CITY_CODES.put("14", "山西"); + CITY_CODES.put("15", "内蒙古"); + CITY_CODES.put("21", "辽宁"); + CITY_CODES.put("22", "吉林"); + CITY_CODES.put("23", "黑龙江"); + CITY_CODES.put("31", "上海"); + CITY_CODES.put("32", "江苏"); + CITY_CODES.put("33", "浙江"); + CITY_CODES.put("34", "安徽"); + CITY_CODES.put("35", "福建"); + CITY_CODES.put("36", "江西"); + CITY_CODES.put("37", "山东"); + CITY_CODES.put("41", "河南"); + CITY_CODES.put("42", "湖北"); + CITY_CODES.put("43", "湖南"); + CITY_CODES.put("44", "广东"); + CITY_CODES.put("45", "广西"); + CITY_CODES.put("46", "海南"); + CITY_CODES.put("50", "重庆"); + CITY_CODES.put("51", "四川"); + CITY_CODES.put("52", "贵州"); + CITY_CODES.put("53", "云南"); + CITY_CODES.put("54", "西藏"); + CITY_CODES.put("61", "陕西"); + CITY_CODES.put("62", "甘肃"); + CITY_CODES.put("63", "青海"); + CITY_CODES.put("64", "宁夏"); + CITY_CODES.put("65", "新疆"); + CITY_CODES.put("71", "台湾"); + CITY_CODES.put("81", "香港"); + CITY_CODES.put("82", "澳门"); + //issue#1277,台湾身份证号码以83开头,但是行政区划为71 + CITY_CODES.put("83", "台湾"); + CITY_CODES.put("91", "国外"); + + TW_FIRST_CODE.put('A', 10); + TW_FIRST_CODE.put('B', 11); + TW_FIRST_CODE.put('C', 12); + TW_FIRST_CODE.put('D', 13); + TW_FIRST_CODE.put('E', 14); + TW_FIRST_CODE.put('F', 15); + TW_FIRST_CODE.put('G', 16); + TW_FIRST_CODE.put('H', 17); + TW_FIRST_CODE.put('J', 18); + TW_FIRST_CODE.put('K', 19); + TW_FIRST_CODE.put('L', 20); + TW_FIRST_CODE.put('M', 21); + TW_FIRST_CODE.put('N', 22); + TW_FIRST_CODE.put('P', 23); + TW_FIRST_CODE.put('Q', 24); + TW_FIRST_CODE.put('R', 25); + TW_FIRST_CODE.put('S', 26); + TW_FIRST_CODE.put('T', 27); + TW_FIRST_CODE.put('U', 28); + TW_FIRST_CODE.put('V', 29); + TW_FIRST_CODE.put('X', 30); + TW_FIRST_CODE.put('Y', 31); + TW_FIRST_CODE.put('W', 32); + TW_FIRST_CODE.put('Z', 33); + TW_FIRST_CODE.put('I', 34); + TW_FIRST_CODE.put('O', 35); + } + + /** + * 将15位身份证号码转换为18位 + * + * @param idCard 15位身份编码 + * @return 18位身份编码 + */ + public static String convert15To18(String idCard) { + StringBuilder idCard18; + if (idCard.length() != CHINA_ID_MIN_LENGTH) { + return null; + } + if (ReUtil.isMatch(PatternPool.NUMBERS, idCard)) { + // 获取出生年月日 + String birthday = idCard.substring(6, 12); + Date birthDate = DateUtil.parse(birthday, "yyMMdd"); + // 获取出生年(完全表现形式,如:2010) + int sYear = DateUtil.year(birthDate); + if (sYear > 2000) { + // 2000年之后不存在15位身份证号,此处用于修复此问题的判断 + sYear -= 100; + } + idCard18 = StrUtil.builder().append(idCard, 0, 6).append(sYear).append(idCard.substring(8)); + // 获取校验位 + char sVal = getCheckCode18(idCard18.toString()); + idCard18.append(sVal); + } else { + return null; + } + return idCard18.toString(); + } + + /** + * 将18位身份证号码转换为15位 + * + * @param idCard 18位身份编码 + * @return 15位身份编码 + */ + public static String convert18To15(String idCard) { + if (StrUtil.isNotBlank(idCard) && IdcardUtil.isValidCard18(idCard)) { + return idCard.substring(0, 6) + idCard.substring(8, idCard.length() - 1); + } + return idCard; + } + + /** + * 是否有效身份证号,忽略X的大小写
+ * 如果身份证号码中含有空格始终返回{@code false} + * + * @param idCard 身份证号,支持18位、15位和港澳台的10位 + * @return 是否有效 + */ + public static boolean isValidCard(String idCard) { + if (StrUtil.isBlank(idCard)) { + return false; + } + + //idCard = idCard.trim(); + int length = idCard.length(); + switch (length) { + case 18:// 18位身份证 + return isValidCard18(idCard); + case 15:// 15位身份证 + return isValidCard15(idCard); + case 10: {// 10位身份证,港澳台地区 + String[] cardVal = isValidCard10(idCard); + return null != cardVal && "true".equals(cardVal[2]); + } + default: + return false; + } + } + + /** + *

+ * 判断18位身份证的合法性 + *

+ * 根据〖中华人民共和国国家标准GB11643-1999〗中有关公民身份号码的规定,公民身份号码是特征组合码,由十七位数字本体码和一位数字校验码组成。
+ * 排列顺序从左至右依次为:六位数字地址码,八位数字出生日期码,三位数字顺序码和一位数字校验码。 + *

+ * 顺序码: 表示在同一地址码所标识的区域范围内,对同年、同月、同 日出生的人编定的顺序号,顺序码的奇数分配给男性,偶数分配 给女性。 + *

+ *
    + *
  1. 第1、2位数字表示:所在省份的代码
  2. + *
  3. 第3、4位数字表示:所在城市的代码
  4. + *
  5. 第5、6位数字表示:所在区县的代码
  6. + *
  7. 第7~14位数字表示:出生年、月、日
  8. + *
  9. 第15、16位数字表示:所在地的派出所的代码
  10. + *
  11. 第17位数字表示性别:奇数表示男性,偶数表示女性
  12. + *
  13. 第18位数字是校检码,用来检验身份证的正确性。校检码可以是0~9的数字,有时也用x表示
  14. + *
+ *

+ * 第十八位数字(校验码)的计算方法为: + *

    + *
  1. 将前面的身份证号码17位数分别乘以不同的系数。从第一位到第十七位的系数分别为:7 9 10 5 8 4 2 1 6 3 7 9 10 5 8 4 2
  2. + *
  3. 将这17位数字和系数相乘的结果相加
  4. + *
  5. 用加出来和除以11,看余数是多少
  6. + *
  7. 余数只可能有0 1 2 3 4 5 6 7 8 9 10这11个数字。其分别对应的最后一位身份证的号码为1 0 X 9 8 7 6 5 4 3 2
  8. + *
  9. 通过上面得知如果余数是2,就会在身份证的第18位数字上出现罗马数字的Ⅹ。如果余数是10,身份证的最后一位号码就是2
  10. + *
+ *
    + *
  1. 香港人在大陆的身份证,【810000】开头;同样可以直接获取到 性别、出生日期
  2. + *
  3. 81000019980902013X: 文绎循 男 1998-09-02
  4. + *
  5. 810000201011210153: 辛烨 男 2010-11-21
  6. + *
+ *
    + *
  1. 澳门人在大陆的身份证,【820000】开头;同样可以直接获取到 性别、出生日期
  2. + *
  3. 820000200009100032: 黄敬杰 男 2000-09-10
  4. + *
+ *
    + *
  1. 台湾人在大陆的身份证,【830000】开头;同样可以直接获取到 性别、出生日期
  2. + *
  3. 830000200209060065: 王宜妃 女 2002-09-06
  4. + *
  5. 830000194609150010: 苏建文 男 1946-09-14
  6. + *
  7. 83000019810715006X: 刁婉琇 女 1981-07-15
  8. + *
+ * + * @param idcard 待验证的身份证 + * @return 是否有效的18位身份证,忽略x的大小写 + */ + public static boolean isValidCard18(String idcard) { + return isValidCard18(idcard, true); + } + + /** + *

+ * 判断18位身份证的合法性 + *

+ * 根据〖中华人民共和国国家标准GB11643-1999〗中有关公民身份号码的规定,公民身份号码是特征组合码,由十七位数字本体码和一位数字校验码组成。
+ * 排列顺序从左至右依次为:六位数字地址码,八位数字出生日期码,三位数字顺序码和一位数字校验码。 + *

+ * 顺序码: 表示在同一地址码所标识的区域范围内,对同年、同月、同 日出生的人编定的顺序号,顺序码的奇数分配给男性,偶数分配 给女性。 + *

+ *
    + *
  1. 第1、2位数字表示:所在省份的代码
  2. + *
  3. 第3、4位数字表示:所在城市的代码
  4. + *
  5. 第5、6位数字表示:所在区县的代码
  6. + *
  7. 第7~14位数字表示:出生年、月、日
  8. + *
  9. 第15、16位数字表示:所在地的派出所的代码
  10. + *
  11. 第17位数字表示性别:奇数表示男性,偶数表示女性
  12. + *
  13. 第18位数字是校检码,用来检验身份证的正确性。校检码可以是0~9的数字,有时也用x表示
  14. + *
+ *

+ * 第十八位数字(校验码)的计算方法为: + *

    + *
  1. 将前面的身份证号码17位数分别乘以不同的系数。从第一位到第十七位的系数分别为:7 9 10 5 8 4 2 1 6 3 7 9 10 5 8 4 2
  2. + *
  3. 将这17位数字和系数相乘的结果相加
  4. + *
  5. 用加出来和除以11,看余数是多少
  6. + *
  7. 余数只可能有0 1 2 3 4 5 6 7 8 9 10这11个数字。其分别对应的最后一位身份证的号码为1 0 X 9 8 7 6 5 4 3 2
  8. + *
  9. 通过上面得知如果余数是2,就会在身份证的第18位数字上出现罗马数字的Ⅹ。如果余数是10,身份证的最后一位号码就是2
  10. + *
+ * + * @param idcard 待验证的身份证 + * @param ignoreCase 是否忽略大小写。{@code true}则忽略X大小写,否则严格匹配大写。 + * @return 是否有效的18位身份证 + * @since 5.5.7 + */ + public static boolean isValidCard18(String idcard, boolean ignoreCase) { + if (CHINA_ID_MAX_LENGTH != idcard.length()) { + return false; + } + + // 省份 + final String proCode = idcard.substring(0, 2); + if (null == CITY_CODES.get(proCode)) { + return false; + } + + //校验生日 + if (!Validator.isBirthday(idcard.substring(6, 14))) { + return false; + } + + // 前17位 + final String code17 = idcard.substring(0, 17); + if (ReUtil.isMatch(PatternPool.NUMBERS, code17)) { + // 获取校验位 + char val = getCheckCode18(code17); + // 第18位 + return CharUtil.equals(val, idcard.charAt(17), ignoreCase); + } + return false; + } + + /** + * 验证15位身份编码是否合法 + * + * @param idcard 身份编码 + * @return 是否合法 + */ + public static boolean isValidCard15(String idcard) { + if (CHINA_ID_MIN_LENGTH != idcard.length()) { + return false; + } + if (ReUtil.isMatch(PatternPool.NUMBERS, idcard)) { + // 省份 + String proCode = idcard.substring(0, 2); + if (null == CITY_CODES.get(proCode)) { + return false; + } + + //校验生日(两位年份,补充为19XX) + return Validator.isBirthday("19" + idcard.substring(6, 12)); + } else { + return false; + } + } + + /** + * 验证10位身份编码是否合法 + * + * @param idcard 身份编码 + * @return 身份证信息数组 + *

+ * [0] - 台湾、澳门、香港 [1] - 性别(男M,女F,未知N) [2] - 是否合法(合法true,不合法false) 若不是身份证件号码则返回null + *

+ */ + public static String[] isValidCard10(String idcard) { + if (StrUtil.isBlank(idcard)) { + return null; + } + String[] info = new String[3]; + String card = idcard.replaceAll("[()]", ""); + if (card.length() != 8 && card.length() != 9 && idcard.length() != 10) { + return null; + } + if (idcard.matches("^[a-zA-Z][0-9]{9}$")) { // 台湾 + info[0] = "台湾"; + char char2 = idcard.charAt(1); + if ('1' == char2) { + info[1] = "M"; + } else if ('2' == char2) { + info[1] = "F"; + } else { + info[1] = "N"; + info[2] = "false"; + return info; + } + info[2] = isValidTWCard(idcard) ? "true" : "false"; + } else if (idcard.matches("^[157][0-9]{6}\\(?[0-9A-Z]\\)?$")) { // 澳门 + info[0] = "澳门"; + info[1] = "N"; + info[2] = "true"; + } else if (idcard.matches("^[A-Z]{1,2}[0-9]{6}\\(?[0-9A]\\)?$")) { // 香港 + info[0] = "香港"; + info[1] = "N"; + info[2] = isValidHKCard(idcard) ? "true" : "false"; + } else { + return null; + } + return info; + } + + /** + * 验证台湾身份证号码 + * + * @param idcard 身份证号码 + * @return 验证码是否符合 + */ + public static boolean isValidTWCard(String idcard) { + if (null == idcard || idcard.length() != 10) { + return false; + } + final Integer iStart = TW_FIRST_CODE.get(idcard.charAt(0)); + if (null == iStart) { + return false; + } + int sum = iStart / 10 + (iStart % 10) * 9; + + final String mid = idcard.substring(1, 9); + final char[] chars = mid.toCharArray(); + int iflag = 8; + for (char c : chars) { + sum += Integer.parseInt(String.valueOf(c)) * iflag; + iflag--; + } + + final String end = idcard.substring(9, 10); + return (sum % 10 == 0 ? 0 : (10 - sum % 10)) == Integer.parseInt(end); + } + + /** + * 验证香港身份证号码(存在Bug,部份特殊身份证无法检查) + *

+ * 身份证前2位为英文字符,如果只出现一个英文字符则表示第一位是空格,对应数字58 前2位英文字符A-Z分别对应数字10-35 最后一位校验码为0-9的数字加上字符"A","A"代表10 + *

+ *

+ * 将身份证号码全部转换为数字,分别对应乘9-1相加的总和,整除11则证件号码有效 + *

+ * + * @param idcard 身份证号码 + * @return 验证码是否符合 + */ + public static boolean isValidHKCard(String idcard) { + String card = idcard.replaceAll("[()]", ""); + int sum; + if (card.length() == 9) { + sum = (Character.toUpperCase(card.charAt(0)) - 55) * 9 + (Character.toUpperCase(card.charAt(1)) - 55) * 8; + card = card.substring(1, 9); + } else { + sum = 522 + (Character.toUpperCase(card.charAt(0)) - 55) * 8; + } + + // 首字母A-Z,A表示1,以此类推 + String mid = card.substring(1, 7); + String end = card.substring(7, 8); + char[] chars = mid.toCharArray(); + int iflag = 7; + for (char c : chars) { + sum = sum + Integer.parseInt(String.valueOf(c)) * iflag; + iflag--; + } + if ("A".equalsIgnoreCase(end)) { + sum += 10; + } else { + sum += Integer.parseInt(end); + } + return sum % 11 == 0; + } + + /** + * 根据身份编号获取生日,只支持15或18位身份证号码 + * + * @param idcard 身份编号 + * @return 生日(yyyyMMdd) + * @see #getBirth(String) + */ + public static String getBirthByIdCard(String idcard) { + return getBirth(idcard); + } + + /** + * 根据身份编号获取生日,只支持15或18位身份证号码 + * + * @param idCard 身份编号 + * @return 生日(yyyyMMdd) + */ + public static String getBirth(String idCard) { + Assert.notBlank(idCard, "id card must be not blank!"); + final int len = idCard.length(); + if (len < CHINA_ID_MIN_LENGTH) { + return null; + } else if (len == CHINA_ID_MIN_LENGTH) { + idCard = convert15To18(idCard); + } + + return Objects.requireNonNull(idCard).substring(6, 14); + } + + /** + * 从身份证号码中获取生日日期,只支持15或18位身份证号码 + * + * @param idCard 身份证号码 + * @return 日期 + */ + public static DateTime getBirthDate(String idCard) { + final String birthByIdCard = getBirthByIdCard(idCard); + return null == birthByIdCard ? null : DateUtil.parse(birthByIdCard, DatePattern.PURE_DATE_FORMAT); + } + + /** + * 根据身份编号获取年龄,只支持15或18位身份证号码 + * + * @param idcard 身份编号 + * @return 年龄 + */ + public static int getAgeByIdCard(String idcard) { + return getAgeByIdCard(idcard, DateUtil.date()); + } + + /** + * 根据身份编号获取指定日期当时的年龄年龄,只支持15或18位身份证号码 + * + * @param idcard 身份编号 + * @param dateToCompare 以此日期为界,计算年龄。 + * @return 年龄 + */ + public static int getAgeByIdCard(String idcard, Date dateToCompare) { + String birth = getBirthByIdCard(idcard); + return DateUtil.age(DateUtil.parse(birth, "yyyyMMdd"), dateToCompare); + } + + /** + * 根据身份编号获取生日年,只支持15或18位身份证号码 + * + * @param idcard 身份编号 + * @return 生日(yyyy) + */ + public static Short getYearByIdCard(String idcard) { + final int len = idcard.length(); + if (len < CHINA_ID_MIN_LENGTH) { + return null; + } else if (len == CHINA_ID_MIN_LENGTH) { + idcard = convert15To18(idcard); + } + return Short.valueOf(Objects.requireNonNull(idcard).substring(6, 10)); + } + + /** + * 根据身份编号获取生日月,只支持15或18位身份证号码 + * + * @param idcard 身份编号 + * @return 生日(MM) + */ + public static Short getMonthByIdCard(String idcard) { + final int len = idcard.length(); + if (len < CHINA_ID_MIN_LENGTH) { + return null; + } else if (len == CHINA_ID_MIN_LENGTH) { + idcard = convert15To18(idcard); + } + return Short.valueOf(Objects.requireNonNull(idcard).substring(10, 12)); + } + + /** + * 根据身份编号获取生日天,只支持15或18位身份证号码 + * + * @param idcard 身份编号 + * @return 生日(dd) + */ + public static Short getDayByIdCard(String idcard) { + final int len = idcard.length(); + if (len < CHINA_ID_MIN_LENGTH) { + return null; + } else if (len == CHINA_ID_MIN_LENGTH) { + idcard = convert15To18(idcard); + } + return Short.valueOf(Objects.requireNonNull(idcard).substring(12, 14)); + } + + /** + * 根据身份编号获取性别,只支持15或18位身份证号码 + * + * @param idcard 身份编号 + * @return 性别(1 : 男 , 0 : 女) + */ + public static int getGenderByIdCard(String idcard) { + Assert.notBlank(idcard); + final int len = idcard.length(); + if (len < CHINA_ID_MIN_LENGTH) { + throw new IllegalArgumentException("ID Card length must be 15 or 18"); + } + + if (len == CHINA_ID_MIN_LENGTH) { + idcard = convert15To18(idcard); + } + char sCardChar = Objects.requireNonNull(idcard).charAt(16); + return (sCardChar % 2 != 0) ? 1 : 0; + } + + /** + * 根据身份编号获取户籍省份编码,只支持15或18位身份证号码 + * + * @param idcard 身份编码 + * @return 省份编码 + * @since 5.7.2 + */ + public static String getProvinceCodeByIdCard(String idcard) { + int len = idcard.length(); + if (len == CHINA_ID_MIN_LENGTH || len == CHINA_ID_MAX_LENGTH) { + return idcard.substring(0, 2); + } + return null; + } + + /** + * 根据身份编号获取户籍省份,只支持15或18位身份证号码 + * + * @param idcard 身份编码 + * @return 省份名称。 + */ + public static String getProvinceByIdCard(String idcard) { + final String code = getProvinceCodeByIdCard(idcard); + if (StrUtil.isNotBlank(code)) { + return CITY_CODES.get(code); + } + return null; + } + + /** + * 根据身份编号获取地市级编码,只支持15或18位身份证号码
+ * 获取编码为4位 + * + * @param idcard 身份编码 + * @return 地市级编码 + */ + public static String getCityCodeByIdCard(String idcard) { + int len = idcard.length(); + if (len == CHINA_ID_MIN_LENGTH || len == CHINA_ID_MAX_LENGTH) { + return idcard.substring(0, 4); + } + return null; + } + + /** + * 根据身份编号获取区县级编码,只支持15或18位身份证号码
+ * 获取编码为6位 + * + * @param idcard 身份编码 + * @return 地市级编码 + * @since 5.8.0 + */ + public static String getDistrictCodeByIdCard(String idcard) { + int len = idcard.length(); + if (len == CHINA_ID_MIN_LENGTH || len == CHINA_ID_MAX_LENGTH) { + return idcard.substring(0, 6); + } + return null; + } + + /** + * 隐藏指定位置的几个身份证号数字为“*” + * + * @param idcard 身份证号 + * @param startInclude 开始位置(包含) + * @param endExclude 结束位置(不包含) + * @return 隐藏后的身份证号码 + * @see StrUtil#hide(CharSequence, int, int) + * @since 3.2.2 + */ + public static String hide(String idcard, int startInclude, int endExclude) { + return StrUtil.hide(idcard, startInclude, endExclude); + } + + /** + * 获取身份证信息,包括身份、城市代码、生日、性别等 + * + * @param idcard 15或18位身份证 + * @return {@link Idcard} + * @since 5.4.3 + */ + public static Idcard getIdcardInfo(String idcard) { + return new Idcard(idcard); + } + + // ----------------------------------------------------------------------------------- Private method start + + /** + * 获得18位身份证校验码 + * + * @param code17 18位身份证号中的前17位 + * @return 第18位 + */ + private static char getCheckCode18(String code17) { + int sum = getPowerSum(code17.toCharArray()); + return getCheckCode18(sum); + } + + /** + * 将power和值与11取模获得余数进行校验码判断 + * + * @param iSum 加权和 + * @return 校验位 + */ + private static char getCheckCode18(int iSum) { + switch (iSum % 11) { + case 10: + return '2'; + case 9: + return '3'; + case 8: + return '4'; + case 7: + return '5'; + case 6: + return '6'; + case 5: + return '7'; + case 4: + return '8'; + case 3: + return '9'; + case 2: + return 'X'; + case 1: + return '0'; + case 0: + return '1'; + default: + return StrUtil.C_SPACE; + } + } + + /** + * 将身份证的每位和对应位的加权因子相乘之后,再得到和值 + * + * @param iArr 身份证号码的数组 + * @return 身份证编码 + */ + private static int getPowerSum(char[] iArr) { + int iSum = 0; + if (POWER.length == iArr.length) { + for (int i = 0; i < iArr.length; i++) { + iSum += Integer.parseInt(String.valueOf(iArr[i])) * POWER[i]; + } + } + return iSum; + } + // ----------------------------------------------------------------------------------- Private method end + + /** + * 身份证信息,包括身份、城市代码、生日、性别等 + * + * @author looly + * @since 5.4.3 + */ + public static class Idcard implements Serializable { + private static final long serialVersionUID = 1L; + + private final String provinceCode; + private final String cityCode; + private final DateTime birthDate; + private final Integer gender; + private final int age; + + /** + * 构造 + * + * @param idcard 身份证号码 + */ + public Idcard(String idcard) { + this.provinceCode = IdcardUtil.getProvinceCodeByIdCard(idcard); + this.cityCode = IdcardUtil.getCityCodeByIdCard(idcard); + this.birthDate = IdcardUtil.getBirthDate(idcard); + this.gender = IdcardUtil.getGenderByIdCard(idcard); + this.age = IdcardUtil.getAgeByIdCard(idcard); + } + + /** + * 获取省份代码 + * + * @return 省份代码 + */ + public String getProvinceCode() { + return this.provinceCode; + } + + /** + * 获取省份名称 + * + * @return 省份代码 + */ + public String getProvince() { + return CITY_CODES.get(this.provinceCode); + } + + /** + * 获取市级编码 + * + * @return 市级编码 + */ + public String getCityCode() { + return this.cityCode; + } + + /** + * 获得生日日期 + * + * @return 生日日期 + */ + public DateTime getBirthDate() { + return this.birthDate; + } + + /** + * 获取性别代号,性别(1 : 男 , 0 : 女) + * + * @return 性别(1 : 男 , 0 : 女) + */ + public Integer getGender() { + return this.gender; + } + + /** + * 获取年龄 + * + * @return 年龄 + */ + public int getAge() { + return age; + } + + @Override + public String toString() { + return "Idcard{" + + "provinceCode='" + provinceCode + '\'' + + ", cityCode='" + cityCode + '\'' + + ", birthDate=" + birthDate + + ", gender=" + gender + + ", age=" + age + + '}'; + } + } +} diff --git a/src/main/java/cn/hutool/core/util/ModifierUtil.java b/src/main/java/cn/hutool/core/util/ModifierUtil.java new file mode 100644 index 0000000..78f6b65 --- /dev/null +++ b/src/main/java/cn/hutool/core/util/ModifierUtil.java @@ -0,0 +1,335 @@ +package cn.hutool.core.util; + +import cn.hutool.core.exceptions.UtilException; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; + +/** + * 修饰符工具类 + * + * @author looly + * @since 4.0.5 + */ +public class ModifierUtil { + + /** + * 修饰符枚举 + * + * @author looly + * @since 4.0.5 + */ + public enum ModifierType { + /** + * public修饰符,所有类都能访问 + */ + PUBLIC(Modifier.PUBLIC), + /** + * private修饰符,只能被自己访问和修改 + */ + PRIVATE(Modifier.PRIVATE), + /** + * protected修饰符,自身、子类及同一个包中类可以访问 + */ + PROTECTED(Modifier.PROTECTED), + /** + * static修饰符,(静态修饰符)指定变量被所有对象共享,即所有实例都可以使用该变量。变量属于这个类 + */ + STATIC(Modifier.STATIC), + /** + * final修饰符,最终修饰符,指定此变量的值不能变,使用在方法上表示不能被重载 + */ + FINAL(Modifier.FINAL), + /** + * synchronized,同步修饰符,在多个线程中,该修饰符用于在运行前,对他所属的方法加锁,以防止其他线程的访问,运行结束后解锁。 + */ + SYNCHRONIZED(Modifier.SYNCHRONIZED), + /** + * (易失修饰符)指定该变量可以同时被几个线程控制和修改 + */ + VOLATILE(Modifier.VOLATILE), + /** + * (过度修饰符)指定该变量是系统保留,暂无特别作用的临时性变量,序列化时忽略 + */ + TRANSIENT(Modifier.TRANSIENT), + /** + * native,本地修饰符。指定此方法的方法体是用其他语言在程序外部编写的。 + */ + NATIVE(Modifier.NATIVE), + + /** + * abstract,将一个类声明为抽象类,没有实现的方法,需要子类提供方法实现。 + */ + ABSTRACT(Modifier.ABSTRACT), + /** + * strictfp,一旦使用了关键字strictfp来声明某个类、接口或者方法时,那么在这个关键字所声明的范围内所有浮点运算都是精确的,符合IEEE-754规范的。 + */ + STRICT(Modifier.STRICT); + + /** + * 修饰符枚举对应的int修饰符值 + */ + private final int value; + + /** + * 构造 + * + * @param modifier 修饰符int表示,见{@link Modifier} + */ + ModifierType(int modifier) { + this.value = modifier; + } + + /** + * 获取修饰符枚举对应的int修饰符值,值见{@link Modifier} + * + * @return 修饰符枚举对应的int修饰符值 + */ + public int getValue() { + return this.value; + } + } + + /** + * 是否同时存在一个或多个修饰符(可能有多个修饰符,如果有指定的修饰符则返回true) + * + * @param clazz 类 + * @param modifierTypes 修饰符枚举 + * @return 是否有指定修饰符,如果有返回true,否则false,如果提供参数为null返回false + */ + public static boolean hasModifier(Class clazz, ModifierType... modifierTypes) { + if (null == clazz || ArrayUtil.isEmpty(modifierTypes)) { + return false; + } + return 0 != (clazz.getModifiers() & modifiersToInt(modifierTypes)); + } + + /** + * 是否同时存在一个或多个修饰符(可能有多个修饰符,如果有指定的修饰符则返回true) + * + * @param constructor 构造方法 + * @param modifierTypes 修饰符枚举 + * @return 是否有指定修饰符,如果有返回true,否则false,如果提供参数为null返回false + */ + public static boolean hasModifier(Constructor constructor, ModifierType... modifierTypes) { + if (null == constructor || ArrayUtil.isEmpty(modifierTypes)) { + return false; + } + return 0 != (constructor.getModifiers() & modifiersToInt(modifierTypes)); + } + + /** + * 是否同时存在一个或多个修饰符(可能有多个修饰符,如果有指定的修饰符则返回true) + * + * @param method 方法 + * @param modifierTypes 修饰符枚举 + * @return 是否有指定修饰符,如果有返回true,否则false,如果提供参数为null返回false + */ + public static boolean hasModifier(Method method, ModifierType... modifierTypes) { + if (null == method || ArrayUtil.isEmpty(modifierTypes)) { + return false; + } + return 0 != (method.getModifiers() & modifiersToInt(modifierTypes)); + } + + /** + * 是否同时存在一个或多个修饰符(可能有多个修饰符,如果有指定的修饰符则返回true) + * + * @param field 字段 + * @param modifierTypes 修饰符枚举 + * @return 是否有指定修饰符,如果有返回true,否则false,如果提供参数为null返回false + */ + public static boolean hasModifier(Field field, ModifierType... modifierTypes) { + if (null == field || ArrayUtil.isEmpty(modifierTypes)) { + return false; + } + return 0 != (field.getModifiers() & modifiersToInt(modifierTypes)); + } + + /** + * 是否是Public字段 + * + * @param field 字段 + * @return 是否是Public + */ + public static boolean isPublic(Field field) { + return hasModifier(field, ModifierType.PUBLIC); + } + + /** + * 是否是Public方法 + * + * @param method 方法 + * @return 是否是Public + */ + public static boolean isPublic(Method method) { + return hasModifier(method, ModifierType.PUBLIC); + } + + /** + * 是否是Public类 + * + * @param clazz 类 + * @return 是否是Public + */ + public static boolean isPublic(Class clazz) { + return hasModifier(clazz, ModifierType.PUBLIC); + } + + /** + * 是否是Public构造 + * + * @param constructor 构造 + * @return 是否是Public + */ + public static boolean isPublic(Constructor constructor) { + return hasModifier(constructor, ModifierType.PUBLIC); + } + + /** + * 是否是static字段 + * + * @param field 字段 + * @return 是否是static + * @since 4.0.8 + */ + public static boolean isStatic(Field field) { + return hasModifier(field, ModifierType.STATIC); + } + + /** + * 是否是static方法 + * + * @param method 方法 + * @return 是否是static + * @since 4.0.8 + */ + public static boolean isStatic(Method method) { + return hasModifier(method, ModifierType.STATIC); + } + + /** + * 是否是static类 + * + * @param clazz 类 + * @return 是否是static + * @since 4.0.8 + */ + public static boolean isStatic(Class clazz) { + return hasModifier(clazz, ModifierType.STATIC); + } + + /** + * 是否是合成字段(由java编译器生成的) + * + * @param field 字段 + * @return 是否是合成字段 + * @since 5.6.3 + */ + public static boolean isSynthetic(Field field) { + return field.isSynthetic(); + } + + /** + * 是否是合成方法(由java编译器生成的) + * + * @param method 方法 + * @return 是否是合成方法 + * @since 5.6.3 + */ + public static boolean isSynthetic(Method method) { + return method.isSynthetic(); + } + + /** + * 是否是合成类(由java编译器生成的) + * + * @param clazz 类 + * @return 是否是合成 + * @since 5.6.3 + */ + public static boolean isSynthetic(Class clazz) { + return clazz.isSynthetic(); + } + + /** + * 是否抽象方法 + * + * @param method 方法 + * @return 是否抽象方法 + * @since 5.7.23 + */ + public static boolean isAbstract(Method method) { + return hasModifier(method, ModifierType.ABSTRACT); + } + + /** + * 设置final的field字段可以被修改 + * 只要不会被编译器内联优化的 final 属性就可以通过反射有效的进行修改 -- 修改后代码中可使用到新的值; + *

以下属性,编译器会内联优化,无法通过反射修改:

+ *
    + *
  • 基本类型 byte, char, short, int, long, float, double, boolean
  • + *
  • Literal String 类型(直接双引号字符串)
  • + *
+ *

以下属性,可以通过反射修改:

+ *
    + *
  • 基本类型的包装类 Byte、Character、Short、Long、Float、Double、Boolean
  • + *
  • 字符串,通过 new String("")实例化
  • + *
  • 自定义java类
  • + *
+ *
+	 * {@code
+	 *      //示例,移除final修饰符
+	 *      class JdbcDialects {private static final List dialects = new ArrayList<>();}
+	 *      Field field = ReflectUtil.getField(JdbcDialects.class, fieldName);
+	 * 		ReflectUtil.removeFinalModify(field);
+	 * 		ReflectUtil.setFieldValue(JdbcDialects.class, fieldName, dialects);
+	 *    }
+	 * 
+ * + * @param field 被修改的field,不可以为空 + * @throws UtilException IllegalAccessException等异常包装 + * @author dazer + * @since 5.8.8 + */ + public static void removeFinalModify(Field field) { + if (field != null) { + if (hasModifier(field, ModifierType.FINAL)) { + //将字段的访问权限设为true:即去除private修饰符的影响 + if (!field.isAccessible()) { + field.setAccessible(true); + } + try { + //去除final修饰符的影响,将字段设为可修改的 + final Field modifiersField = Field.class.getDeclaredField("modifiers"); + //Field 的 modifiers 是私有的 + modifiersField.setAccessible(true); + //& :位与运算符,按位与; 运算规则:两个数都转为二进制,然后从高位开始比较,如果两个数都为1则为1,否则为0。 + //~ :位非运算符,按位取反;运算规则:转成二进制,如果位为0,结果是1,如果位为1,结果是0. + modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL); + } catch (final NoSuchFieldException | IllegalAccessException e) { + //内部,工具类,基本不抛出异常 + throw new UtilException(e, "IllegalAccess for {}.{}", field.getDeclaringClass(), field.getName()); + } + } + } + } + //-------------------------------------------------------------------------------------------------------- Private method start + + /** + * 多个修饰符做“与”操作,表示同时存在多个修饰符 + * + * @param modifierTypes 修饰符列表,元素不能为空 + * @return “与”之后的修饰符 + */ + private static int modifiersToInt(ModifierType... modifierTypes) { + int modifier = modifierTypes[0].getValue(); + for (int i = 1; i < modifierTypes.length; i++) { + modifier |= modifierTypes[i].getValue(); + } + return modifier; + } + //-------------------------------------------------------------------------------------------------------- Private method end +} diff --git a/src/main/java/cn/hutool/core/util/NumberUtil.java b/src/main/java/cn/hutool/core/util/NumberUtil.java new file mode 100644 index 0000000..a697102 --- /dev/null +++ b/src/main/java/cn/hutool/core/util/NumberUtil.java @@ -0,0 +1,2801 @@ +package cn.hutool.core.util; + +import cn.hutool.core.exceptions.UtilException; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.math.Calculator; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.math.RoundingMode; +import java.text.DecimalFormat; +import java.text.NumberFormat; +import java.text.ParseException; +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; + +/** + * 数字工具类
+ * 对于精确值计算应该使用 {@link BigDecimal}
+ * JDK7中BigDecimal(double val)构造方法的结果有一定的不可预知性,例如: + * + *
+ * new BigDecimal(0.1)
+ * 
+ *

+ * 表示的不是0.1而是0.1000000000000000055511151231257827021181583404541015625 + * + *

+ * 这是因为0.1无法准确的表示为double。因此应该使用new BigDecimal(String)。 + *

+ * 相关介绍: + *
    + *
  • http://www.oschina.net/code/snippet_563112_25237
  • + *
  • https://github.com/venusdrogon/feilong-core/wiki/one-jdk7-bug-thinking
  • + *
+ * + * @author Looly + */ +public class NumberUtil { + + /** + * 默认除法运算精度 + */ + private static final int DEFAULT_DIV_SCALE = 10; + + /** + * 0-20对应的阶乘,超过20的阶乘会超过Long.MAX_VALUE + */ + private static final long[] FACTORIALS = new long[]{ + 1L, 1L, 2L, 6L, 24L, 120L, 720L, 5040L, 40320L, 362880L, 3628800L, 39916800L, 479001600L, 6227020800L, + 87178291200L, 1307674368000L, 20922789888000L, 355687428096000L, 6402373705728000L, 121645100408832000L, + 2432902008176640000L}; + + /** + * 提供精确的加法运算 + * + * @param v1 被加数 + * @param v2 加数 + * @return 和 + */ + public static double add(float v1, float v2) { + return add(Float.toString(v1), Float.toString(v2)).doubleValue(); + } + + /** + * 提供精确的加法运算 + * + * @param v1 被加数 + * @param v2 加数 + * @return 和 + */ + public static double add(float v1, double v2) { + return add(Float.toString(v1), Double.toString(v2)).doubleValue(); + } + + /** + * 提供精确的加法运算 + * + * @param v1 被加数 + * @param v2 加数 + * @return 和 + */ + public static double add(double v1, float v2) { + return add(Double.toString(v1), Float.toString(v2)).doubleValue(); + } + + /** + * 提供精确的加法运算 + * + * @param v1 被加数 + * @param v2 加数 + * @return 和 + */ + public static double add(double v1, double v2) { + return add(Double.toString(v1), Double.toString(v2)).doubleValue(); + } + + /** + * 提供精确的加法运算 + * + * @param v1 被加数 + * @param v2 加数 + * @return 和 + * @since 3.1.1 + */ + public static double add(Double v1, Double v2) { + //noinspection RedundantCast + return add((Number) v1, (Number) v2).doubleValue(); + } + + /** + * 提供精确的加法运算
+ * 如果传入多个值为null或者空,则返回0 + * + * @param v1 被加数 + * @param v2 加数 + * @return 和 + */ + public static BigDecimal add(Number v1, Number v2) { + return add(new Number[]{v1, v2}); + } + + /** + * 提供精确的加法运算
+ * 如果传入多个值为null或者空,则返回0 + * + * @param values 多个被加值 + * @return 和 + * @since 4.0.0 + */ + public static BigDecimal add(Number... values) { + if (ArrayUtil.isEmpty(values)) { + return BigDecimal.ZERO; + } + + Number value = values[0]; + BigDecimal result = toBigDecimal(value); + for (int i = 1; i < values.length; i++) { + value = values[i]; + if (null != value) { + result = result.add(toBigDecimal(value)); + } + } + return result; + } + + /** + * 提供精确的加法运算
+ * 如果传入多个值为null或者空,则返回0 + * + * @param values 多个被加值 + * @return 和 + * @since 4.0.0 + */ + public static BigDecimal add(String... values) { + if (ArrayUtil.isEmpty(values)) { + return BigDecimal.ZERO; + } + + String value = values[0]; + BigDecimal result = toBigDecimal(value); + for (int i = 1; i < values.length; i++) { + value = values[i]; + if (StrUtil.isNotBlank(value)) { + result = result.add(toBigDecimal(value)); + } + } + return result; + } + + /** + * 提供精确的加法运算
+ * 如果传入多个值为null或者空,则返回0 + * + * @param values 多个被加值 + * @return 和 + * @since 4.0.0 + */ + public static BigDecimal add(BigDecimal... values) { + if (ArrayUtil.isEmpty(values)) { + return BigDecimal.ZERO; + } + + BigDecimal value = values[0]; + BigDecimal result = toBigDecimal(value); + for (int i = 1; i < values.length; i++) { + value = values[i]; + if (null != value) { + result = result.add(value); + } + } + return result; + } + + /** + * 提供精确的减法运算 + * + * @param v1 被减数 + * @param v2 减数 + * @return 差 + */ + public static double sub(float v1, float v2) { + return sub(Float.toString(v1), Float.toString(v2)).doubleValue(); + } + + /** + * 提供精确的减法运算 + * + * @param v1 被减数 + * @param v2 减数 + * @return 差 + */ + public static double sub(float v1, double v2) { + return sub(Float.toString(v1), Double.toString(v2)).doubleValue(); + } + + /** + * 提供精确的减法运算 + * + * @param v1 被减数 + * @param v2 减数 + * @return 差 + */ + public static double sub(double v1, float v2) { + return sub(Double.toString(v1), Float.toString(v2)).doubleValue(); + } + + /** + * 提供精确的减法运算 + * + * @param v1 被减数 + * @param v2 减数 + * @return 差 + */ + public static double sub(double v1, double v2) { + return sub(Double.toString(v1), Double.toString(v2)).doubleValue(); + } + + /** + * 提供精确的减法运算 + * + * @param v1 被减数 + * @param v2 减数 + * @return 差 + */ + public static double sub(Double v1, Double v2) { + //noinspection RedundantCast + return sub((Number) v1, (Number) v2).doubleValue(); + } + + /** + * 提供精确的减法运算
+ * 如果传入多个值为null或者空,则返回0 + * + * @param v1 被减数 + * @param v2 减数 + * @return 差 + */ + public static BigDecimal sub(Number v1, Number v2) { + return sub(new Number[]{v1, v2}); + } + + /** + * 提供精确的减法运算
+ * 如果传入多个值为null或者空,则返回0 + * + * @param values 多个被减值 + * @return 差 + * @since 4.0.0 + */ + public static BigDecimal sub(Number... values) { + if (ArrayUtil.isEmpty(values)) { + return BigDecimal.ZERO; + } + + Number value = values[0]; + BigDecimal result = toBigDecimal(value); + for (int i = 1; i < values.length; i++) { + value = values[i]; + if (null != value) { + result = result.subtract(toBigDecimal(value)); + } + } + return result; + } + + /** + * 提供精确的减法运算
+ * 如果传入多个值为null或者空,则返回0 + * + * @param values 多个被减值 + * @return 差 + * @since 4.0.0 + */ + public static BigDecimal sub(String... values) { + if (ArrayUtil.isEmpty(values)) { + return BigDecimal.ZERO; + } + + String value = values[0]; + BigDecimal result = toBigDecimal(value); + for (int i = 1; i < values.length; i++) { + value = values[i]; + if (StrUtil.isNotBlank(value)) { + result = result.subtract(toBigDecimal(value)); + } + } + return result; + } + + /** + * 提供精确的减法运算
+ * 如果传入多个值为null或者空,则返回0 + * + * @param values 多个被减值 + * @return 差 + * @since 4.0.0 + */ + public static BigDecimal sub(BigDecimal... values) { + if (ArrayUtil.isEmpty(values)) { + return BigDecimal.ZERO; + } + + BigDecimal value = values[0]; + BigDecimal result = toBigDecimal(value); + for (int i = 1; i < values.length; i++) { + value = values[i]; + if (null != value) { + result = result.subtract(value); + } + } + return result; + } + + /** + * 提供精确的乘法运算 + * + * @param v1 被乘数 + * @param v2 乘数 + * @return 积 + */ + public static double mul(float v1, float v2) { + return mul(Float.toString(v1), Float.toString(v2)).doubleValue(); + } + + /** + * 提供精确的乘法运算 + * + * @param v1 被乘数 + * @param v2 乘数 + * @return 积 + */ + public static double mul(float v1, double v2) { + return mul(Float.toString(v1), Double.toString(v2)).doubleValue(); + } + + /** + * 提供精确的乘法运算 + * + * @param v1 被乘数 + * @param v2 乘数 + * @return 积 + */ + public static double mul(double v1, float v2) { + return mul(Double.toString(v1), Float.toString(v2)).doubleValue(); + } + + /** + * 提供精确的乘法运算 + * + * @param v1 被乘数 + * @param v2 乘数 + * @return 积 + */ + public static double mul(double v1, double v2) { + return mul(Double.toString(v1), Double.toString(v2)).doubleValue(); + } + + /** + * 提供精确的乘法运算
+ * 如果传入多个值为null或者空,则返回0 + * + * @param v1 被乘数 + * @param v2 乘数 + * @return 积 + */ + public static double mul(Double v1, Double v2) { + //noinspection RedundantCast + return mul((Number) v1, (Number) v2).doubleValue(); + } + + /** + * 提供精确的乘法运算
+ * 如果传入多个值为null或者空,则返回0 + * + * @param v1 被乘数 + * @param v2 乘数 + * @return 积 + */ + public static BigDecimal mul(Number v1, Number v2) { + return mul(new Number[]{v1, v2}); + } + + /** + * 提供精确的乘法运算
+ * 如果传入多个值为null或者空,则返回0 + * + * @param values 多个被乘值 + * @return 积 + * @since 4.0.0 + */ + public static BigDecimal mul(Number... values) { + if (ArrayUtil.isEmpty(values) || ArrayUtil.hasNull(values)) { + return BigDecimal.ZERO; + } + + Number value = values[0]; + BigDecimal result = new BigDecimal(value.toString()); + for (int i = 1; i < values.length; i++) { + value = values[i]; + result = result.multiply(new BigDecimal(value.toString())); + } + return result; + } + + /** + * 提供精确的乘法运算 + * + * @param v1 被乘数 + * @param v2 乘数 + * @return 积 + * @since 3.0.8 + */ + public static BigDecimal mul(String v1, String v2) { + return mul(new BigDecimal(v1), new BigDecimal(v2)); + } + + /** + * 提供精确的乘法运算
+ * 如果传入多个值为null或者空,则返回0 + * + * @param values 多个被乘值 + * @return 积 + * @since 4.0.0 + */ + public static BigDecimal mul(String... values) { + if (ArrayUtil.isEmpty(values) || ArrayUtil.hasNull(values)) { + return BigDecimal.ZERO; + } + + BigDecimal result = new BigDecimal(values[0]); + for (int i = 1; i < values.length; i++) { + result = result.multiply(new BigDecimal(values[i])); + } + + return result; + } + + /** + * 提供精确的乘法运算
+ * 如果传入多个值为null或者空,则返回0 + * + * @param values 多个被乘值 + * @return 积 + * @since 4.0.0 + */ + public static BigDecimal mul(BigDecimal... values) { + if (ArrayUtil.isEmpty(values) || ArrayUtil.hasNull(values)) { + return BigDecimal.ZERO; + } + + BigDecimal result = values[0]; + for (int i = 1; i < values.length; i++) { + result = result.multiply(values[i]); + } + return result; + } + + /** + * 提供(相对)精确的除法运算,当发生除不尽的情况的时候,精确到小数点后10位,后面的四舍五入 + * + * @param v1 被除数 + * @param v2 除数 + * @return 两个参数的商 + */ + public static double div(float v1, float v2) { + return div(v1, v2, DEFAULT_DIV_SCALE); + } + + /** + * 提供(相对)精确的除法运算,当发生除不尽的情况的时候,精确到小数点后10位,后面的四舍五入 + * + * @param v1 被除数 + * @param v2 除数 + * @return 两个参数的商 + */ + public static double div(float v1, double v2) { + return div(v1, v2, DEFAULT_DIV_SCALE); + } + + /** + * 提供(相对)精确的除法运算,当发生除不尽的情况的时候,精确到小数点后10位,后面的四舍五入 + * + * @param v1 被除数 + * @param v2 除数 + * @return 两个参数的商 + */ + public static double div(double v1, float v2) { + return div(v1, v2, DEFAULT_DIV_SCALE); + } + + /** + * 提供(相对)精确的除法运算,当发生除不尽的情况的时候,精确到小数点后10位,后面的四舍五入 + * + * @param v1 被除数 + * @param v2 除数 + * @return 两个参数的商 + */ + public static double div(double v1, double v2) { + return div(v1, v2, DEFAULT_DIV_SCALE); + } + + /** + * 提供(相对)精确的除法运算,当发生除不尽的情况的时候,精确到小数点后10位,后面的四舍五入 + * + * @param v1 被除数 + * @param v2 除数 + * @return 两个参数的商 + */ + public static double div(Double v1, Double v2) { + return div(v1, v2, DEFAULT_DIV_SCALE); + } + + /** + * 提供(相对)精确的除法运算,当发生除不尽的情况的时候,精确到小数点后10位,后面的四舍五入 + * + * @param v1 被除数 + * @param v2 除数 + * @return 两个参数的商 + * @since 3.1.0 + */ + public static BigDecimal div(Number v1, Number v2) { + return div(v1, v2, DEFAULT_DIV_SCALE); + } + + /** + * 提供(相对)精确的除法运算,当发生除不尽的情况的时候,精确到小数点后10位,后面的四舍五入 + * + * @param v1 被除数 + * @param v2 除数 + * @return 两个参数的商 + */ + public static BigDecimal div(String v1, String v2) { + return div(v1, v2, DEFAULT_DIV_SCALE); + } + + /** + * 提供(相对)精确的除法运算,当发生除不尽的情况时,由scale指定精确度,后面的四舍五入 + * + * @param v1 被除数 + * @param v2 除数 + * @param scale 精确度,如果为负值,取绝对值 + * @return 两个参数的商 + */ + public static double div(float v1, float v2, int scale) { + return div(v1, v2, scale, RoundingMode.HALF_UP); + } + + /** + * 提供(相对)精确的除法运算,当发生除不尽的情况时,由scale指定精确度,后面的四舍五入 + * + * @param v1 被除数 + * @param v2 除数 + * @param scale 精确度,如果为负值,取绝对值 + * @return 两个参数的商 + */ + public static double div(float v1, double v2, int scale) { + return div(v1, v2, scale, RoundingMode.HALF_UP); + } + + /** + * 提供(相对)精确的除法运算,当发生除不尽的情况时,由scale指定精确度,后面的四舍五入 + * + * @param v1 被除数 + * @param v2 除数 + * @param scale 精确度,如果为负值,取绝对值 + * @return 两个参数的商 + */ + public static double div(double v1, float v2, int scale) { + return div(v1, v2, scale, RoundingMode.HALF_UP); + } + + /** + * 提供(相对)精确的除法运算,当发生除不尽的情况时,由scale指定精确度,后面的四舍五入 + * + * @param v1 被除数 + * @param v2 除数 + * @param scale 精确度,如果为负值,取绝对值 + * @return 两个参数的商 + */ + public static double div(double v1, double v2, int scale) { + return div(v1, v2, scale, RoundingMode.HALF_UP); + } + + /** + * 提供(相对)精确的除法运算,当发生除不尽的情况时,由scale指定精确度,后面的四舍五入 + * + * @param v1 被除数 + * @param v2 除数 + * @param scale 精确度,如果为负值,取绝对值 + * @return 两个参数的商 + */ + public static double div(Double v1, Double v2, int scale) { + return div(v1, v2, scale, RoundingMode.HALF_UP); + } + + /** + * 提供(相对)精确的除法运算,当发生除不尽的情况时,由scale指定精确度,后面的四舍五入 + * + * @param v1 被除数 + * @param v2 除数 + * @param scale 精确度,如果为负值,取绝对值 + * @return 两个参数的商 + * @since 3.1.0 + */ + public static BigDecimal div(Number v1, Number v2, int scale) { + return div(v1, v2, scale, RoundingMode.HALF_UP); + } + + /** + * 提供(相对)精确的除法运算,当发生除不尽的情况时,由scale指定精确度,后面的四舍五入 + * + * @param v1 被除数 + * @param v2 除数 + * @param scale 精确度,如果为负值,取绝对值 + * @return 两个参数的商 + */ + public static BigDecimal div(String v1, String v2, int scale) { + return div(v1, v2, scale, RoundingMode.HALF_UP); + } + + /** + * 提供(相对)精确的除法运算,当发生除不尽的情况时,由scale指定精确度 + * + * @param v1 被除数 + * @param v2 除数 + * @param scale 精确度,如果为负值,取绝对值 + * @param roundingMode 保留小数的模式 {@link RoundingMode} + * @return 两个参数的商 + */ + public static double div(float v1, float v2, int scale, RoundingMode roundingMode) { + return div(Float.toString(v1), Float.toString(v2), scale, roundingMode).doubleValue(); + } + + /** + * 提供(相对)精确的除法运算,当发生除不尽的情况时,由scale指定精确度 + * + * @param v1 被除数 + * @param v2 除数 + * @param scale 精确度,如果为负值,取绝对值 + * @param roundingMode 保留小数的模式 {@link RoundingMode} + * @return 两个参数的商 + */ + public static double div(float v1, double v2, int scale, RoundingMode roundingMode) { + return div(Float.toString(v1), Double.toString(v2), scale, roundingMode).doubleValue(); + } + + /** + * 提供(相对)精确的除法运算,当发生除不尽的情况时,由scale指定精确度 + * + * @param v1 被除数 + * @param v2 除数 + * @param scale 精确度,如果为负值,取绝对值 + * @param roundingMode 保留小数的模式 {@link RoundingMode} + * @return 两个参数的商 + */ + public static double div(double v1, float v2, int scale, RoundingMode roundingMode) { + return div(Double.toString(v1), Float.toString(v2), scale, roundingMode).doubleValue(); + } + + /** + * 提供(相对)精确的除法运算,当发生除不尽的情况时,由scale指定精确度 + * + * @param v1 被除数 + * @param v2 除数 + * @param scale 精确度,如果为负值,取绝对值 + * @param roundingMode 保留小数的模式 {@link RoundingMode} + * @return 两个参数的商 + */ + public static double div(double v1, double v2, int scale, RoundingMode roundingMode) { + return div(Double.toString(v1), Double.toString(v2), scale, roundingMode).doubleValue(); + } + + /** + * 提供(相对)精确的除法运算,当发生除不尽的情况时,由scale指定精确度 + * + * @param v1 被除数 + * @param v2 除数 + * @param scale 精确度,如果为负值,取绝对值 + * @param roundingMode 保留小数的模式 {@link RoundingMode} + * @return 两个参数的商 + */ + public static double div(Double v1, Double v2, int scale, RoundingMode roundingMode) { + //noinspection RedundantCast + return div((Number) v1, (Number) v2, scale, roundingMode).doubleValue(); + } + + /** + * 提供(相对)精确的除法运算,当发生除不尽的情况时,由scale指定精确度 + * + * @param v1 被除数 + * @param v2 除数 + * @param scale 精确度,如果为负值,取绝对值 + * @param roundingMode 保留小数的模式 {@link RoundingMode} + * @return 两个参数的商 + * @since 3.1.0 + */ + public static BigDecimal div(Number v1, Number v2, int scale, RoundingMode roundingMode) { + if (v1 instanceof BigDecimal && v2 instanceof BigDecimal) { + return div((BigDecimal) v1, (BigDecimal) v2, scale, roundingMode); + } + return div(StrUtil.toStringOrNull(v1), StrUtil.toStringOrNull(v2), scale, roundingMode); + } + + /** + * 提供(相对)精确的除法运算,当发生除不尽的情况时,由scale指定精确度 + * + * @param v1 被除数 + * @param v2 除数 + * @param scale 精确度,如果为负值,取绝对值 + * @param roundingMode 保留小数的模式 {@link RoundingMode} + * @return 两个参数的商 + */ + public static BigDecimal div(String v1, String v2, int scale, RoundingMode roundingMode) { + return div(toBigDecimal(v1), toBigDecimal(v2), scale, roundingMode); + } + + /** + * 提供(相对)精确的除法运算,当发生除不尽的情况时,由scale指定精确度 + * + * @param v1 被除数 + * @param v2 除数 + * @param scale 精确度,如果为负值,取绝对值 + * @param roundingMode 保留小数的模式 {@link RoundingMode} + * @return 两个参数的商 + * @since 3.0.9 + */ + public static BigDecimal div(BigDecimal v1, BigDecimal v2, int scale, RoundingMode roundingMode) { + Assert.notNull(v2, "Divisor must be not null !"); + if (null == v1) { + return BigDecimal.ZERO; + } + if (scale < 0) { + scale = -scale; + } + return v1.divide(v2, scale, roundingMode); + } + + /** + * 补充Math.ceilDiv() JDK8中添加了和Math.floorDiv()但却没有ceilDiv() + * + * @param v1 被除数 + * @param v2 除数 + * @return 两个参数的商 + * @since 5.3.3 + */ + public static int ceilDiv(int v1, int v2) { + return (int) Math.ceil((double) v1 / v2); + } + + // ------------------------------------------------------------------------------------------- round + + /** + * 保留固定位数小数
+ * 采用四舍五入策略 {@link RoundingMode#HALF_UP}
+ * 例如保留2位小数:123.456789 =》 123.46 + * + * @param v 值 + * @param scale 保留小数位数 + * @return 新值 + */ + public static BigDecimal round(double v, int scale) { + return round(v, scale, RoundingMode.HALF_UP); + } + + /** + * 保留固定位数小数
+ * 采用四舍五入策略 {@link RoundingMode#HALF_UP}
+ * 例如保留2位小数:123.456789 =》 123.46 + * + * @param v 值 + * @param scale 保留小数位数 + * @return 新值 + */ + public static String roundStr(double v, int scale) { + return round(v, scale).toPlainString(); + } + + /** + * 保留固定位数小数
+ * 采用四舍五入策略 {@link RoundingMode#HALF_UP}
+ * 例如保留2位小数:123.456789 =》 123.46 + * + * @param numberStr 数字值的字符串表现形式 + * @param scale 保留小数位数 + * @return 新值 + */ + public static BigDecimal round(String numberStr, int scale) { + return round(numberStr, scale, RoundingMode.HALF_UP); + } + + /** + * 保留固定位数小数
+ * 采用四舍五入策略 {@link RoundingMode#HALF_UP}
+ * 例如保留2位小数:123.456789 =》 123.46 + * + * @param number 数字值 + * @param scale 保留小数位数 + * @return 新值 + * @since 4.1.0 + */ + public static BigDecimal round(BigDecimal number, int scale) { + return round(number, scale, RoundingMode.HALF_UP); + } + + /** + * 保留固定位数小数
+ * 采用四舍五入策略 {@link RoundingMode#HALF_UP}
+ * 例如保留2位小数:123.456789 =》 123.46 + * + * @param numberStr 数字值的字符串表现形式 + * @param scale 保留小数位数 + * @return 新值 + * @since 3.2.2 + */ + public static String roundStr(String numberStr, int scale) { + return round(numberStr, scale).toPlainString(); + } + + /** + * 保留固定位数小数
+ * 例如保留四位小数:123.456789 =》 123.4567 + * + * @param v 值 + * @param scale 保留小数位数 + * @param roundingMode 保留小数的模式 {@link RoundingMode} + * @return 新值 + */ + public static BigDecimal round(double v, int scale, RoundingMode roundingMode) { + return round(Double.toString(v), scale, roundingMode); + } + + /** + * 保留固定位数小数
+ * 例如保留四位小数:123.456789 =》 123.4567 + * + * @param v 值 + * @param scale 保留小数位数 + * @param roundingMode 保留小数的模式 {@link RoundingMode} + * @return 新值 + * @since 3.2.2 + */ + public static String roundStr(double v, int scale, RoundingMode roundingMode) { + return round(v, scale, roundingMode).toPlainString(); + } + + /** + * 保留固定位数小数
+ * 例如保留四位小数:123.456789 =》 123.4567 + * + * @param numberStr 数字值的字符串表现形式 + * @param scale 保留小数位数,如果传入小于0,则默认0 + * @param roundingMode 保留小数的模式 {@link RoundingMode},如果传入null则默认四舍五入 + * @return 新值 + */ + public static BigDecimal round(String numberStr, int scale, RoundingMode roundingMode) { + Assert.notBlank(numberStr); + if (scale < 0) { + scale = 0; + } + return round(toBigDecimal(numberStr), scale, roundingMode); + } + + /** + * 保留固定位数小数
+ * 例如保留四位小数:123.456789 =》 123.4567 + * + * @param number 数字值 + * @param scale 保留小数位数,如果传入小于0,则默认0 + * @param roundingMode 保留小数的模式 {@link RoundingMode},如果传入null则默认四舍五入 + * @return 新值 + */ + public static BigDecimal round(BigDecimal number, int scale, RoundingMode roundingMode) { + if (null == number) { + number = BigDecimal.ZERO; + } + if (scale < 0) { + scale = 0; + } + if (null == roundingMode) { + roundingMode = RoundingMode.HALF_UP; + } + + return number.setScale(scale, roundingMode); + } + + /** + * 保留固定位数小数
+ * 例如保留四位小数:123.456789 =》 123.4567 + * + * @param numberStr 数字值的字符串表现形式 + * @param scale 保留小数位数 + * @param roundingMode 保留小数的模式 {@link RoundingMode} + * @return 新值 + * @since 3.2.2 + */ + public static String roundStr(String numberStr, int scale, RoundingMode roundingMode) { + return round(numberStr, scale, roundingMode).toPlainString(); + } + + /** + * 四舍六入五成双计算法 + *

+ * 四舍六入五成双是一种比较精确比较科学的计数保留法,是一种数字修约规则。 + *

+ * + *
+	 * 算法规则:
+	 * 四舍六入五考虑,
+	 * 五后非零就进一,
+	 * 五后皆零看奇偶,
+	 * 五前为偶应舍去,
+	 * 五前为奇要进一。
+	 * 
+ * + * @param number 需要科学计算的数据 + * @param scale 保留的小数位 + * @return 结果 + * @since 4.1.0 + */ + public static BigDecimal roundHalfEven(Number number, int scale) { + return roundHalfEven(toBigDecimal(number), scale); + } + + /** + * 四舍六入五成双计算法 + *

+ * 四舍六入五成双是一种比较精确比较科学的计数保留法,是一种数字修约规则。 + *

+ * + *
+	 * 算法规则:
+	 * 四舍六入五考虑,
+	 * 五后非零就进一,
+	 * 五后皆零看奇偶,
+	 * 五前为偶应舍去,
+	 * 五前为奇要进一。
+	 * 
+ * + * @param value 需要科学计算的数据 + * @param scale 保留的小数位 + * @return 结果 + * @since 4.1.0 + */ + public static BigDecimal roundHalfEven(BigDecimal value, int scale) { + return round(value, scale, RoundingMode.HALF_EVEN); + } + + /** + * 保留固定小数位数,舍去多余位数 + * + * @param number 需要科学计算的数据 + * @param scale 保留的小数位 + * @return 结果 + * @since 4.1.0 + */ + public static BigDecimal roundDown(Number number, int scale) { + return roundDown(toBigDecimal(number), scale); + } + + /** + * 保留固定小数位数,舍去多余位数 + * + * @param value 需要科学计算的数据 + * @param scale 保留的小数位 + * @return 结果 + * @since 4.1.0 + */ + public static BigDecimal roundDown(BigDecimal value, int scale) { + return round(value, scale, RoundingMode.DOWN); + } + + // ------------------------------------------------------------------------------------------- decimalFormat + + /** + * 格式化double
+ * 对 {@link DecimalFormat} 做封装
+ * + * @param pattern 格式 格式中主要以 # 和 0 两种占位符号来指定数字长度。0 表示如果位数不足则以 0 填充,# 表示只要有可能就把数字拉上这个位置。
+ *
    + *
  • 0 =》 取一位整数
  • + *
  • 0.00 =》 取一位整数和两位小数
  • + *
  • 00.000 =》 取两位整数和三位小数
  • + *
  • # =》 取所有整数部分
  • + *
  • #.##% =》 以百分比方式计数,并取两位小数
  • + *
  • #.#####E0 =》 显示为科学计数法,并取五位小数
  • + *
  • ,### =》 每三位以逗号进行分隔,例如:299,792,458
  • + *
  • 光速大小为每秒,###米 =》 将格式嵌入文本
  • + *
+ * @param value 值 + * @return 格式化后的值 + */ + public static String decimalFormat(String pattern, double value) { + Assert.isTrue(isValid(value), "value is NaN or Infinite!"); + return new DecimalFormat(pattern).format(value); + } + + /** + * 格式化double
+ * 对 {@link DecimalFormat} 做封装
+ * + * @param pattern 格式 格式中主要以 # 和 0 两种占位符号来指定数字长度。0 表示如果位数不足则以 0 填充,# 表示只要有可能就把数字拉上这个位置。
+ *
    + *
  • 0 =》 取一位整数
  • + *
  • 0.00 =》 取一位整数和两位小数
  • + *
  • 00.000 =》 取两位整数和三位小数
  • + *
  • # =》 取所有整数部分
  • + *
  • #.##% =》 以百分比方式计数,并取两位小数
  • + *
  • #.#####E0 =》 显示为科学计数法,并取五位小数
  • + *
  • ,### =》 每三位以逗号进行分隔,例如:299,792,458
  • + *
  • 光速大小为每秒,###米 =》 将格式嵌入文本
  • + *
+ * @param value 值 + * @return 格式化后的值 + * @since 3.0.5 + */ + public static String decimalFormat(String pattern, long value) { + return new DecimalFormat(pattern).format(value); + } + + /** + * 格式化double
+ * 对 {@link DecimalFormat} 做封装
+ * + * @param pattern 格式 格式中主要以 # 和 0 两种占位符号来指定数字长度。0 表示如果位数不足则以 0 填充,# 表示只要有可能就把数字拉上这个位置。
+ *
    + *
  • 0 =》 取一位整数
  • + *
  • 0.00 =》 取一位整数和两位小数
  • + *
  • 00.000 =》 取两位整数和三位小数
  • + *
  • # =》 取所有整数部分
  • + *
  • #.##% =》 以百分比方式计数,并取两位小数
  • + *
  • #.#####E0 =》 显示为科学计数法,并取五位小数
  • + *
  • ,### =》 每三位以逗号进行分隔,例如:299,792,458
  • + *
  • 光速大小为每秒,###米 =》 将格式嵌入文本
  • + *
+ * @param value 值,支持BigDecimal、BigInteger、Number等类型 + * @return 格式化后的值 + * @since 5.1.6 + */ + public static String decimalFormat(String pattern, Object value) { + return decimalFormat(pattern, value, null); + } + + /** + * 格式化double
+ * 对 {@link DecimalFormat} 做封装
+ * + * @param pattern 格式 格式中主要以 # 和 0 两种占位符号来指定数字长度。0 表示如果位数不足则以 0 填充,# 表示只要有可能就把数字拉上这个位置。
+ *
    + *
  • 0 =》 取一位整数
  • + *
  • 0.00 =》 取一位整数和两位小数
  • + *
  • 00.000 =》 取两位整数和三位小数
  • + *
  • # =》 取所有整数部分
  • + *
  • #.##% =》 以百分比方式计数,并取两位小数
  • + *
  • #.#####E0 =》 显示为科学计数法,并取五位小数
  • + *
  • ,### =》 每三位以逗号进行分隔,例如:299,792,458
  • + *
  • 光速大小为每秒,###米 =》 将格式嵌入文本
  • + *
+ * @param value 值,支持BigDecimal、BigInteger、Number等类型 + * @param roundingMode 保留小数的方式枚举 + * @return 格式化后的值 + * @since 5.6.5 + */ + public static String decimalFormat(String pattern, Object value, RoundingMode roundingMode) { + if (value instanceof Number) { + Assert.isTrue(isValidNumber((Number) value), "value is NaN or Infinite!"); + } + final DecimalFormat decimalFormat = new DecimalFormat(pattern); + if (null != roundingMode) { + decimalFormat.setRoundingMode(roundingMode); + } + return decimalFormat.format(value); + } + + /** + * 格式化金额输出,每三位用逗号分隔 + * + * @param value 金额 + * @return 格式化后的值 + * @since 3.0.9 + */ + public static String decimalFormatMoney(double value) { + return decimalFormat(",##0.00", value); + } + + /** + * 格式化百分比,小数采用四舍五入方式 + * + * @param number 值 + * @param scale 保留小数位数 + * @return 百分比 + * @since 3.2.3 + */ + public static String formatPercent(double number, int scale) { + final NumberFormat format = NumberFormat.getPercentInstance(); + format.setMaximumFractionDigits(scale); + return format.format(number); + } + + // ------------------------------------------------------------------------------------------- isXXX + + /** + * 是否为数字,支持包括: + * + *
+	 * 1、10进制
+	 * 2、16进制数字(0x开头)
+	 * 3、科学计数法形式(1234E3)
+	 * 4、类型标识形式(123D)
+	 * 5、正负数标识形式(+123、-234)
+	 * 
+ * + * @param str 字符串值 + * @return 是否为数字 + */ + public static boolean isNumber(CharSequence str) { + if (StrUtil.isBlank(str)) { + return false; + } + char[] chars = str.toString().toCharArray(); + int sz = chars.length; + boolean hasExp = false; + boolean hasDecPoint = false; + boolean allowSigns = false; + boolean foundDigit = false; + // deal with any possible sign up front + int start = (chars[0] == '-' || chars[0] == '+') ? 1 : 0; + if (sz > start + 1) { + if (chars[start] == '0' && (chars[start + 1] == 'x' || chars[start + 1] == 'X')) { + int i = start + 2; + if (i == sz) { + return false; // str == "0x" + } + // checking hex (it can't be anything else) + for (; i < chars.length; i++) { + if ((chars[i] < '0' || chars[i] > '9') && (chars[i] < 'a' || chars[i] > 'f') && (chars[i] < 'A' || chars[i] > 'F')) { + return false; + } + } + return true; + } + } + sz--; // don't want to loop to the last char, check it afterwords + // for type qualifiers + int i = start; + // loop to the next to last char or to the last char if we need another digit to + // make a valid number (e.g. chars[0..5] = "1234E") + while (i < sz || (i < sz + 1 && allowSigns && !foundDigit)) { + if (chars[i] >= '0' && chars[i] <= '9') { + foundDigit = true; + allowSigns = false; + + } else if (chars[i] == '.') { + if (hasDecPoint || hasExp) { + // two decimal points or dec in exponent + return false; + } + hasDecPoint = true; + } else if (chars[i] == 'e' || chars[i] == 'E') { + // we've already taken care of hex. + if (hasExp) { + // two E's + return false; + } + if (!foundDigit) { + return false; + } + hasExp = true; + allowSigns = true; + } else if (chars[i] == '+' || chars[i] == '-') { + if (!allowSigns) { + return false; + } + allowSigns = false; + foundDigit = false; // we need a digit after the E + } else { + return false; + } + i++; + } + if (i < chars.length) { + if (chars[i] >= '0' && chars[i] <= '9') { + // no type qualifier, OK + return true; + } + if (chars[i] == 'e' || chars[i] == 'E') { + // can't have an E at the last byte + return false; + } + if (chars[i] == '.') { + if (hasDecPoint || hasExp) { + // two decimal points or dec in exponent + return false; + } + // single trailing decimal point after non-exponent is ok + return foundDigit; + } + if (!allowSigns && (chars[i] == 'd' || chars[i] == 'D' || chars[i] == 'f' || chars[i] == 'F')) { + return foundDigit; + } + if (chars[i] == 'l' || chars[i] == 'L') { + // not allowing L with an exponent + return foundDigit && !hasExp; + } + // last character is illegal + return false; + } + // allowSigns is true iff the val ends in 'E' + // found digit it to make sure weird stuff like '.' and '1E-' doesn't pass + return !allowSigns && foundDigit; + } + + /** + * 判断String是否是整数
+ * 支持10进制 + * + * @param s String + * @return 是否为整数 + */ + public static boolean isInteger(String s) { + if (StrUtil.isBlank(s)) { + return false; + } + try { + Integer.parseInt(s); + } catch (NumberFormatException e) { + return false; + } + return true; + } + + /** + * 判断字符串是否是Long类型
+ * 支持10进制 + * + * @param s String + * @return 是否为{@link Long}类型 + * @since 4.0.0 + */ + public static boolean isLong(String s) { + if (StrUtil.isBlank(s)) { + return false; + } + try { + Long.parseLong(s); + } catch (NumberFormatException e) { + return false; + } + return true; + } + + /** + * 判断字符串是否是浮点数 + * + * @param s String + * @return 是否为{@link Double}类型 + */ + public static boolean isDouble(String s) { + if (StrUtil.isBlank(s)) { + return false; + } + try { + Double.parseDouble(s); + } catch (NumberFormatException ignore) { + return false; + } + return s.contains("."); + } + + /** + * 是否是质数(素数)
+ * 质数表的质数又称素数。指整数在一个大于1的自然数中,除了1和此整数自身外,没法被其他自然数整除的数。 + * + * @param n 数字 + * @return 是否是质数 + */ + public static boolean isPrimes(int n) { + Assert.isTrue(n > 1, "The number must be > 1"); + for (int i = 2; i <= Math.sqrt(n); i++) { + if (n % i == 0) { + return false; + } + } + return true; + } + + // ------------------------------------------------------------------------------------------- generateXXX + + /** + * 生成不重复随机数 根据给定的最小数字和最大数字,以及随机数的个数,产生指定的不重复的数组 + * + * @param begin 最小数字(包含该数) + * @param end 最大数字(不包含该数) + * @param size 指定产生随机数的个数 + * @return 随机int数组 + */ + public static int[] generateRandomNumber(int begin, int end, int size) { + // 种子你可以随意生成,但不能重复 + final int[] seed = ArrayUtil.range(begin, end); + return generateRandomNumber(begin, end, size, seed); + } + + /** + * 生成不重复随机数 根据给定的最小数字和最大数字,以及随机数的个数,产生指定的不重复的数组 + * + * @param begin 最小数字(包含该数) + * @param end 最大数字(不包含该数) + * @param size 指定产生随机数的个数 + * @param seed 种子,用于取随机数的int池 + * @return 随机int数组 + * @since 5.4.5 + */ + public static int[] generateRandomNumber(int begin, int end, int size, int[] seed) { + if (begin > end) { + int temp = begin; + begin = end; + end = temp; + } + // 加入逻辑判断,确保begin= size, "Size is larger than range between begin and end!"); + Assert.isTrue(seed.length >= size, "Size is larger than seed size!"); + + final int[] ranArr = new int[size]; + // 数量你可以自己定义。 + for (int i = 0; i < size; i++) { + // 得到一个位置 + int j = RandomUtil.randomInt(seed.length - i); + // 得到那个位置的数值 + ranArr[i] = seed[j]; + // 将最后一个未用的数字放到这里 + seed[j] = seed[seed.length - 1 - i]; + } + return ranArr; + } + + /** + * 生成不重复随机数 根据给定的最小数字和最大数字,以及随机数的个数,产生指定的不重复的数组 + * + * @param begin 最小数字(包含该数) + * @param end 最大数字(不包含该数) + * @param size 指定产生随机数的个数 + * @return 随机int数组 + */ + public static Integer[] generateBySet(int begin, int end, int size) { + if (begin > end) { + int temp = begin; + begin = end; + end = temp; + } + // 加入逻辑判断,确保begin set = new HashSet<>(size, 1); + while (set.size() < size) { + set.add(begin + RandomUtil.randomInt(end - begin)); + } + + return set.toArray(new Integer[0]); + } + + // ------------------------------------------------------------------------------------------- range + + /** + * 从0开始给定范围内的整数列表,步进为1 + * + * @param stop 结束(包含) + * @return 整数列表 + * @since 3.3.1 + */ + public static int[] range(int stop) { + return range(0, stop); + } + + /** + * 给定范围内的整数列表,步进为1 + * + * @param start 开始(包含) + * @param stop 结束(包含) + * @return 整数列表 + */ + public static int[] range(int start, int stop) { + return range(start, stop, 1); + } + + /** + * 给定范围内的整数列表 + * + * @param start 开始(包含) + * @param stop 结束(包含) + * @param step 步进 + * @return 整数列表 + */ + public static int[] range(int start, int stop, int step) { + if (start < stop) { + step = Math.abs(step); + } else if (start > stop) { + step = -Math.abs(step); + } else {// start == end + return new int[]{start}; + } + + int size = Math.abs((stop - start) / step) + 1; + int[] values = new int[size]; + int index = 0; + for (int i = start; (step > 0) ? i <= stop : i >= stop; i += step) { + values[index] = i; + index++; + } + return values; + } + + /** + * 将给定范围内的整数添加到已有集合中,步进为1 + * + * @param start 开始(包含) + * @param stop 结束(包含) + * @param values 集合 + * @return 集合 + */ + public static Collection appendRange(int start, int stop, Collection values) { + return appendRange(start, stop, 1, values); + } + + /** + * 将给定范围内的整数添加到已有集合中 + * + * @param start 开始(包含) + * @param stop 结束(包含) + * @param step 步进 + * @param values 集合 + * @return 集合 + */ + public static Collection appendRange(int start, int stop, int step, Collection values) { + if (start < stop) { + step = Math.abs(step); + } else if (start > stop) { + step = -Math.abs(step); + } else {// start == end + values.add(start); + return values; + } + + for (int i = start; (step > 0) ? i <= stop : i >= stop; i += step) { + values.add(i); + } + return values; + } + + // ------------------------------------------------------------------------------------------- others + + /** + * 计算阶乘 + *

+ * n! = n * (n-1) * ... * 2 * 1 + *

+ * + * @param n 阶乘起始 + * @return 结果 + * @since 5.6.0 + */ + public static BigInteger factorial(BigInteger n) { + if (n.equals(BigInteger.ZERO)) { + return BigInteger.ONE; + } + return factorial(n, BigInteger.ZERO); + } + + /** + * 计算范围阶乘 + *

+ * factorial(start, end) = start * (start - 1) * ... * (end + 1) + *

+ * + * @param start 阶乘起始(包含) + * @param end 阶乘结束,必须小于起始(不包括) + * @return 结果 + * @since 5.6.0 + */ + public static BigInteger factorial(BigInteger start, BigInteger end) { + Assert.notNull(start, "Factorial start must be not null!"); + Assert.notNull(end, "Factorial end must be not null!"); + if (start.compareTo(BigInteger.ZERO) < 0 || end.compareTo(BigInteger.ZERO) < 0) { + throw new IllegalArgumentException(StrUtil.format("Factorial start and end both must be > 0, but got start={}, end={}", start, end)); + } + + if (start.equals(BigInteger.ZERO)) { + start = BigInteger.ONE; + } + + if (end.compareTo(BigInteger.ONE) < 0) { + end = BigInteger.ONE; + } + + BigInteger result = start; + end = end.add(BigInteger.ONE); + while (start.compareTo(end) > 0) { + start = start.subtract(BigInteger.ONE); + result = result.multiply(start); + } + return result; + } + + /** + * 计算范围阶乘 + *

+ * factorial(start, end) = start * (start - 1) * ... * (end + 1) + *

+ * + * @param start 阶乘起始(包含) + * @param end 阶乘结束,必须小于起始(不包括) + * @return 结果 + * @since 4.1.0 + */ + public static long factorial(long start, long end) { + // 负数没有阶乘 + if (start < 0 || end < 0) { + throw new IllegalArgumentException(StrUtil.format("Factorial start and end both must be >= 0, but got start={}, end={}", start, end)); + } + if (0L == start || start == end) { + return 1L; + } + if (start < end) { + return 0L; + } + return factorialMultiplyAndCheck(start, factorial(start - 1, end)); + } + + /** + * 计算范围阶乘中校验中间的计算是否存在溢出,factorial提前做了负数和0的校验,因此这里没有校验数字的正负 + * + * @param a 乘数 + * @param b 被乘数 + * @return 如果 a * b的结果没有溢出直接返回,否则抛出异常 + */ + private static long factorialMultiplyAndCheck(long a, long b) { + if (a <= Long.MAX_VALUE / b) { + return a * b; + } + throw new IllegalArgumentException(StrUtil.format("Overflow in multiplication: {} * {}", a, b)); + } + + /** + * 计算阶乘 + *

+ * n! = n * (n-1) * ... * 2 * 1 + *

+ * + * @param n 阶乘起始 + * @return 结果 + */ + public static long factorial(long n) { + if (n < 0 || n > 20) { + throw new IllegalArgumentException(StrUtil.format("Factorial must have n >= 0 and n <= 20 for n!, but got n = {}", n)); + } + return FACTORIALS[(int) n]; + } + + /** + * 平方根算法
+ * 推荐使用 {@link Math#sqrt(double)} + * + * @param x 值 + * @return 平方根 + */ + public static long sqrt(long x) { + long y = 0; + long b = (~Long.MAX_VALUE) >>> 1; + while (b > 0) { + if (x >= y + b) { + x -= y + b; + y >>= 1; + y += b; + } else { + y >>= 1; + } + b >>= 2; + } + return y; + } + + /** + * 可以用于计算双色球、大乐透注数的方法
+ * 比如大乐透35选5可以这样调用processMultiple(7,5); 就是数学中的:C75=7*6/2*1 + * + * @param selectNum 选中小球个数 + * @param minNum 最少要选中多少个小球 + * @return 注数 + */ + public static int processMultiple(int selectNum, int minNum) { + int result; + result = mathSubNode(selectNum, minNum) / mathNode(selectNum - minNum); + return result; + } + + /** + * 最大公约数 + * + * @param m 第一个值 + * @param n 第二个值 + * @return 最大公约数 + */ + public static int divisor(int m, int n) { + while (m % n != 0) { + int temp = m % n; + m = n; + n = temp; + } + return n; + } + + /** + * 最小公倍数 + * + * @param m 第一个值 + * @param n 第二个值 + * @return 最小公倍数 + */ + public static int multiple(int m, int n) { + return m * n / divisor(m, n); + } + + /** + * 获得数字对应的二进制字符串 + * + * @param number 数字 + * @return 二进制字符串 + */ + public static String getBinaryStr(Number number) { + if (number instanceof Long) { + return Long.toBinaryString((Long) number); + } else if (number instanceof Integer) { + return Integer.toBinaryString((Integer) number); + } else { + return Long.toBinaryString(number.longValue()); + } + } + + /** + * 二进制转int + * + * @param binaryStr 二进制字符串 + * @return int + */ + public static int binaryToInt(String binaryStr) { + return Integer.parseInt(binaryStr, 2); + } + + /** + * 二进制转long + * + * @param binaryStr 二进制字符串 + * @return long + */ + public static long binaryToLong(String binaryStr) { + return Long.parseLong(binaryStr, 2); + } + + // ------------------------------------------------------------------------------------------- compare + + /** + * 比较两个值的大小 + * + * @param x 第一个值 + * @param y 第二个值 + * @return x==y返回0,x<y返回小于0的数,x>y返回大于0的数 + * @see Character#compare(char, char) + * @since 3.0.1 + */ + public static int compare(char x, char y) { + return Character.compare(x, y); + } + + /** + * 比较两个值的大小 + * + * @param x 第一个值 + * @param y 第二个值 + * @return x==y返回0,x<y返回小于0的数,x>y返回大于0的数 + * @see Double#compare(double, double) + * @since 3.0.1 + */ + public static int compare(double x, double y) { + return Double.compare(x, y); + } + + /** + * 比较两个值的大小 + * + * @param x 第一个值 + * @param y 第二个值 + * @return x==y返回0,x<y返回小于0的数,x>y返回大于0的数 + * @see Integer#compare(int, int) + * @since 3.0.1 + */ + public static int compare(int x, int y) { + return Integer.compare(x, y); + } + + /** + * 比较两个值的大小 + * + * @param x 第一个值 + * @param y 第二个值 + * @return x==y返回0,x<y返回小于0的数,x>y返回大于0的数 + * @see Long#compare(long, long) + * @since 3.0.1 + */ + public static int compare(long x, long y) { + return Long.compare(x, y); + } + + /** + * 比较两个值的大小 + * + * @param x 第一个值 + * @param y 第二个值 + * @return x==y返回0,x<y返回小于0的数,x>y返回大于0的数 + * @see Short#compare(short, short) + * @since 3.0.1 + */ + public static int compare(short x, short y) { + return Short.compare(x, y); + } + + /** + * 比较两个值的大小 + * + * @param x 第一个值 + * @param y 第二个值 + * @return x==y返回0,x<y返回-1,x>y返回1 + * @see Byte#compare(byte, byte) + * @since 3.0.1 + */ + public static int compare(byte x, byte y) { + return Byte.compare(x, y); + } + + /** + * 比较大小,参数1 > 参数2 返回true + * + * @param bigNum1 数字1 + * @param bigNum2 数字2 + * @return 是否大于 + * @since 3.0.9 + */ + public static boolean isGreater(BigDecimal bigNum1, BigDecimal bigNum2) { + Assert.notNull(bigNum1); + Assert.notNull(bigNum2); + return bigNum1.compareTo(bigNum2) > 0; + } + + /** + * 比较大小,参数1 >= 参数2 返回true + * + * @param bigNum1 数字1 + * @param bigNum2 数字2 + * @return 是否大于等于 + * @since 3, 0.9 + */ + public static boolean isGreaterOrEqual(BigDecimal bigNum1, BigDecimal bigNum2) { + Assert.notNull(bigNum1); + Assert.notNull(bigNum2); + return bigNum1.compareTo(bigNum2) >= 0; + } + + /** + * 比较大小,参数1 < 参数2 返回true + * + * @param bigNum1 数字1 + * @param bigNum2 数字2 + * @return 是否小于 + * @since 3, 0.9 + */ + public static boolean isLess(BigDecimal bigNum1, BigDecimal bigNum2) { + Assert.notNull(bigNum1); + Assert.notNull(bigNum2); + return bigNum1.compareTo(bigNum2) < 0; + } + + /** + * 比较大小,参数1<=参数2 返回true + * + * @param bigNum1 数字1 + * @param bigNum2 数字2 + * @return 是否小于等于 + * @since 3, 0.9 + */ + public static boolean isLessOrEqual(BigDecimal bigNum1, BigDecimal bigNum2) { + Assert.notNull(bigNum1); + Assert.notNull(bigNum2); + return bigNum1.compareTo(bigNum2) <= 0; + } + + /** + * 检查值是否在指定范围内 + * + * @param value 值 + * @param minInclude 最小值(包含) + * @param maxInclude 最大值(包含) + * @return 经过检查后的值 + * @since 5.8.5 + */ + public static boolean isIn(final BigDecimal value, final BigDecimal minInclude, final BigDecimal maxInclude) { + Assert.notNull(value); + Assert.notNull(minInclude); + Assert.notNull(maxInclude); + return isGreaterOrEqual(value, minInclude) && isLessOrEqual(value, maxInclude); + } + + /** + * 比较大小,值相等 返回true
+ * 此方法通过调用{@link Double#doubleToLongBits(double)}方法来判断是否相等
+ * 此方法判断值相等时忽略精度的,即0.00 == 0 + * + * @param num1 数字1 + * @param num2 数字2 + * @return 是否相等 + * @since 5.4.2 + */ + public static boolean equals(double num1, double num2) { + return Double.doubleToLongBits(num1) == Double.doubleToLongBits(num2); + } + + /** + * 比较大小,值相等 返回true
+ * 此方法通过调用{@link Float#floatToIntBits(float)}方法来判断是否相等
+ * 此方法判断值相等时忽略精度的,即0.00 == 0 + * + * @param num1 数字1 + * @param num2 数字2 + * @return 是否相等 + * @since 5.4.5 + */ + public static boolean equals(float num1, float num2) { + return Float.floatToIntBits(num1) == Float.floatToIntBits(num2); + } + + /** + * 比较大小,值相等 返回true
+ * 此方法修复传入long型数据由于没有本类型重载方法,导致数据精度丢失 + * + * @param num1 数字1 + * @param num2 数字2 + * @return 是否相等 + * @since 5.7.19 + */ + public static boolean equals(long num1, long num2) { + return num1 == num2; + } + + /** + * 比较大小,值相等 返回true
+ * 此方法通过调用{@link BigDecimal#compareTo(BigDecimal)}方法来判断是否相等
+ * 此方法判断值相等时忽略精度的,即0.00 == 0 + * + * @param bigNum1 数字1 + * @param bigNum2 数字2 + * @return 是否相等 + */ + public static boolean equals(BigDecimal bigNum1, BigDecimal bigNum2) { + //noinspection NumberEquality + if (bigNum1 == bigNum2) { + // 如果用户传入同一对象,省略compareTo以提高性能。 + return true; + } + if (bigNum1 == null || bigNum2 == null) { + return false; + } + return 0 == bigNum1.compareTo(bigNum2); + } + + /** + * 比较两个字符是否相同 + * + * @param c1 字符1 + * @param c2 字符2 + * @param ignoreCase 是否忽略大小写 + * @return 是否相同 + * @see CharUtil#equals(char, char, boolean) + * @since 3.2.1 + */ + public static boolean equals(char c1, char c2, boolean ignoreCase) { + return CharUtil.equals(c1, c2, ignoreCase); + } + + /** + * 取最小值 + * + * @param 元素类型 + * @param numberArray 数字数组 + * @return 最小值 + * @see ArrayUtil#min(Comparable[]) + * @since 4.0.7 + */ + public static > T min(T[] numberArray) { + return ArrayUtil.min(numberArray); + } + + /** + * 取最小值 + * + * @param numberArray 数字数组 + * @return 最小值 + * @see ArrayUtil#min(long...) + * @since 4.0.7 + */ + public static long min(long... numberArray) { + return ArrayUtil.min(numberArray); + } + + /** + * 取最小值 + * + * @param numberArray 数字数组 + * @return 最小值 + * @see ArrayUtil#min(int...) + * @since 4.0.7 + */ + public static int min(int... numberArray) { + return ArrayUtil.min(numberArray); + } + + /** + * 取最小值 + * + * @param numberArray 数字数组 + * @return 最小值 + * @see ArrayUtil#min(short...) + * @since 4.0.7 + */ + public static short min(short... numberArray) { + return ArrayUtil.min(numberArray); + } + + /** + * 取最小值 + * + * @param numberArray 数字数组 + * @return 最小值 + * @see ArrayUtil#min(double...) + * @since 4.0.7 + */ + public static double min(double... numberArray) { + return ArrayUtil.min(numberArray); + } + + /** + * 取最小值 + * + * @param numberArray 数字数组 + * @return 最小值 + * @see ArrayUtil#min(float...) + * @since 4.0.7 + */ + public static float min(float... numberArray) { + return ArrayUtil.min(numberArray); + } + + /** + * 取最小值 + * + * @param numberArray 数字数组 + * @return 最小值 + * @see ArrayUtil#min(Comparable[]) + * @since 5.0.8 + */ + public static BigDecimal min(BigDecimal... numberArray) { + return ArrayUtil.min(numberArray); + } + + /** + * 取最大值 + * + * @param 元素类型 + * @param numberArray 数字数组 + * @return 最大值 + * @see ArrayUtil#max(Comparable[]) + * @since 4.0.7 + */ + public static > T max(T[] numberArray) { + return ArrayUtil.max(numberArray); + } + + /** + * 取最大值 + * + * @param numberArray 数字数组 + * @return 最大值 + * @see ArrayUtil#max(long...) + * @since 4.0.7 + */ + public static long max(long... numberArray) { + return ArrayUtil.max(numberArray); + } + + /** + * 取最大值 + * + * @param numberArray 数字数组 + * @return 最大值 + * @see ArrayUtil#max(int...) + * @since 4.0.7 + */ + public static int max(int... numberArray) { + return ArrayUtil.max(numberArray); + } + + /** + * 取最大值 + * + * @param numberArray 数字数组 + * @return 最大值 + * @see ArrayUtil#max(short...) + * @since 4.0.7 + */ + public static short max(short... numberArray) { + return ArrayUtil.max(numberArray); + } + + /** + * 取最大值 + * + * @param numberArray 数字数组 + * @return 最大值 + * @see ArrayUtil#max(double...) + * @since 4.0.7 + */ + public static double max(double... numberArray) { + return ArrayUtil.max(numberArray); + } + + /** + * 取最大值 + * + * @param numberArray 数字数组 + * @return 最大值 + * @see ArrayUtil#max(float...) + * @since 4.0.7 + */ + public static float max(float... numberArray) { + return ArrayUtil.max(numberArray); + } + + /** + * 取最大值 + * + * @param numberArray 数字数组 + * @return 最大值 + * @see ArrayUtil#max(Comparable[]) + * @since 5.0.8 + */ + public static BigDecimal max(BigDecimal... numberArray) { + return ArrayUtil.max(numberArray); + } + + /** + * 数字转字符串
+ * 调用{@link Number#toString()},并去除尾小数点儿后多余的0 + * + * @param number A Number + * @param defaultValue 如果number参数为{@code null},返回此默认值 + * @return A String. + * @since 3.0.9 + */ + public static String toStr(Number number, String defaultValue) { + return (null == number) ? defaultValue : toStr(number); + } + + /** + * 数字转字符串
+ * 调用{@link Number#toString()}或 {@link BigDecimal#toPlainString()},并去除尾小数点儿后多余的0 + * + * @param number A Number + * @return A String. + */ + public static String toStr(Number number) { + return toStr(number, true); + } + + /** + * 数字转字符串
+ * 调用{@link Number#toString()}或 {@link BigDecimal#toPlainString()},并去除尾小数点儿后多余的0 + * + * @param number A Number + * @param isStripTrailingZeros 是否去除末尾多余0,例如5.0返回5 + * @return A String. + */ + public static String toStr(Number number, boolean isStripTrailingZeros) { + Assert.notNull(number, "Number is null !"); + + // BigDecimal单独处理,使用非科学计数法 + if (number instanceof BigDecimal) { + return toStr((BigDecimal) number, isStripTrailingZeros); + } + + Assert.isTrue(isValidNumber(number), "Number is non-finite!"); + // 去掉小数点儿后多余的0 + String string = number.toString(); + if (isStripTrailingZeros) { + if (string.indexOf('.') > 0 && string.indexOf('e') < 0 && string.indexOf('E') < 0) { + while (string.endsWith("0")) { + string = string.substring(0, string.length() - 1); + } + if (string.endsWith(".")) { + string = string.substring(0, string.length() - 1); + } + } + } + return string; + } + + /** + * {@link BigDecimal}数字转字符串
+ * 调用{@link BigDecimal#toPlainString()},并去除尾小数点儿后多余的0 + * + * @param bigDecimal A {@link BigDecimal} + * @return A String. + * @since 5.4.6 + */ + public static String toStr(BigDecimal bigDecimal) { + return toStr(bigDecimal, true); + } + + /** + * {@link BigDecimal}数字转字符串
+ * 调用{@link BigDecimal#toPlainString()},可选去除尾小数点儿后多余的0 + * + * @param bigDecimal A {@link BigDecimal} + * @param isStripTrailingZeros 是否去除末尾多余0,例如5.0返回5 + * @return A String. + * @since 5.4.6 + */ + public static String toStr(BigDecimal bigDecimal, boolean isStripTrailingZeros) { + Assert.notNull(bigDecimal, "BigDecimal is null !"); + if (isStripTrailingZeros) { + bigDecimal = bigDecimal.stripTrailingZeros(); + } + return bigDecimal.toPlainString(); + } + + /** + * 数字转{@link BigDecimal}
+ * Float、Double等有精度问题,转换为字符串后再转换
+ * null转换为0 + * + * @param number 数字 + * @return {@link BigDecimal} + * @since 4.0.9 + */ + public static BigDecimal toBigDecimal(Number number) { + if (null == number) { + return BigDecimal.ZERO; + } + + if (number instanceof BigDecimal) { + return (BigDecimal) number; + } else if (number instanceof Long) { + return new BigDecimal((Long) number); + } else if (number instanceof Integer) { + return new BigDecimal((Integer) number); + } else if (number instanceof BigInteger) { + return new BigDecimal((BigInteger) number); + } + + // Float、Double等有精度问题,转换为字符串后再转换 + return toBigDecimal(number.toString()); + } + + /** + * 数字转{@link BigDecimal}
+ * null或""或空白符转换为0 + * + * @param numberStr 数字字符串 + * @return {@link BigDecimal} + * @since 4.0.9 + */ + public static BigDecimal toBigDecimal(String numberStr) { + if (StrUtil.isBlank(numberStr)) { + return BigDecimal.ZERO; + } + + try { + // 支持类似于 1,234.55 格式的数字 + final Number number = parseNumber(numberStr); + if (number instanceof BigDecimal) { + return (BigDecimal) number; + } else { + return new BigDecimal(number.toString()); + } + } catch (Exception ignore) { + // 忽略解析错误 + } + + return new BigDecimal(numberStr); + } + + /** + * 数字转{@link BigInteger}
+ * null转换为0 + * + * @param number 数字 + * @return {@link BigInteger} + * @since 5.4.5 + */ + public static BigInteger toBigInteger(Number number) { + if (null == number) { + return BigInteger.ZERO; + } + + if (number instanceof BigInteger) { + return (BigInteger) number; + } else if (number instanceof Long) { + return BigInteger.valueOf((Long) number); + } + + return toBigInteger(number.longValue()); + } + + /** + * 数字转{@link BigInteger}
+ * null或""或空白符转换为0 + * + * @param number 数字字符串 + * @return {@link BigInteger} + * @since 5.4.5 + */ + public static BigInteger toBigInteger(String number) { + return StrUtil.isBlank(number) ? BigInteger.ZERO : new BigInteger(number); + } + + /** + * 计算等份个数 + * + * @param total 总数 + * @param part 每份的个数 + * @return 分成了几份 + * @since 3.0.6 + */ + public static int count(int total, int part) { + return (total % part == 0) ? (total / part) : (total / part + 1); + } + + /** + * 空转0 + * + * @param decimal {@link BigDecimal},可以为{@code null} + * @return {@link BigDecimal}参数为空时返回0的值 + * @since 3.0.9 + */ + public static BigDecimal null2Zero(BigDecimal decimal) { + + return decimal == null ? BigDecimal.ZERO : decimal; + } + + /** + * 如果给定值为0,返回1,否则返回原值 + * + * @param value 值 + * @return 1或非0值 + * @since 3.1.2 + */ + public static int zero2One(int value) { + return 0 == value ? 1 : value; + } + + /** + * 创建{@link BigInteger},支持16进制、10进制和8进制,如果传入空白串返回null
+ * from Apache Common Lang + * + * @param str 数字字符串 + * @return {@link BigInteger} + * @since 3.2.1 + */ + public static BigInteger newBigInteger(String str) { + str = StrUtil.trimToNull(str); + if (null == str) { + return null; + } + + int pos = 0; // 数字字符串位置 + int radix = 10; + boolean negate = false; // 负数与否 + if (str.startsWith("-")) { + negate = true; + pos = 1; + } + if (str.startsWith("0x", pos) || str.startsWith("0X", pos)) { + // hex + radix = 16; + pos += 2; + } else if (str.startsWith("#", pos)) { + // alternative hex (allowed by Long/Integer) + radix = 16; + pos++; + } else if (str.startsWith("0", pos) && str.length() > pos + 1) { + // octal; so long as there are additional digits + radix = 8; + pos++; + } // default is to treat as decimal + + if (pos > 0) { + str = str.substring(pos); + } + final BigInteger value = new BigInteger(str, radix); + return negate ? value.negate() : value; + } + + /** + * 判断两个数字是否相邻,例如1和2相邻,1和3不相邻
+ * 判断方法为做差取绝对值判断是否为1 + * + * @param number1 数字1 + * @param number2 数字2 + * @return 是否相邻 + * @since 4.0.7 + */ + public static boolean isBeside(long number1, long number2) { + return Math.abs(number1 - number2) == 1; + } + + /** + * 判断两个数字是否相邻,例如1和2相邻,1和3不相邻
+ * 判断方法为做差取绝对值判断是否为1 + * + * @param number1 数字1 + * @param number2 数字2 + * @return 是否相邻 + * @since 4.0.7 + */ + public static boolean isBeside(int number1, int number2) { + return Math.abs(number1 - number2) == 1; + } + + /** + * 把给定的总数平均分成N份,返回每份的个数
+ * 当除以分数有余数时每份+1 + * + * @param total 总数 + * @param partCount 份数 + * @return 每份的个数 + * @since 4.0.7 + */ + public static int partValue(int total, int partCount) { + return partValue(total, partCount, true); + } + + /** + * 把给定的总数平均分成N份,返回每份的个数
+ * 如果isPlusOneWhenHasRem为true,则当除以分数有余数时每份+1,否则丢弃余数部分 + * + * @param total 总数 + * @param partCount 份数 + * @param isPlusOneWhenHasRem 在有余数时是否每份+1 + * @return 每份的个数 + * @since 4.0.7 + */ + public static int partValue(int total, int partCount, boolean isPlusOneWhenHasRem) { + int partValue = total / partCount; + if (isPlusOneWhenHasRem && total % partCount > 0) { + partValue++; + } + return partValue; + } + + /** + * 提供精确的幂运算 + * + * @param number 底数 + * @param n 指数 + * @return 幂的积 + * @since 4.1.0 + */ + public static BigDecimal pow(Number number, int n) { + return pow(toBigDecimal(number), n); + } + + /** + * 提供精确的幂运算 + * + * @param number 底数 + * @param n 指数 + * @return 幂的积 + * @since 4.1.0 + */ + public static BigDecimal pow(BigDecimal number, int n) { + return number.pow(n); + } + + + /** + * 判断一个整数是否是2的幂 + * + * @param n 待验证的整数 + * @return 如果n是2的幂返回true, 反之返回false + */ + public static boolean isPowerOfTwo(long n) { + return (n > 0) && ((n & (n - 1)) == 0); + } + + /** + * 解析转换数字字符串为int型数字,规则如下: + * + *
+	 * 1、0x开头的视为16进制数字
+	 * 2、0开头的忽略开头的0
+	 * 3、其它情况按照10进制转换
+	 * 4、空串返回0
+	 * 5、.123形式返回0(按照小于0的小数对待)
+	 * 6、123.56截取小数点之前的数字,忽略小数部分
+	 * 
+ * + * @param number 数字,支持0x开头、0开头和普通十进制 + * @return int + * @throws NumberFormatException 数字格式异常 + * @since 4.1.4 + */ + public static int parseInt(String number) throws NumberFormatException { + if (StrUtil.isBlank(number)) { + return 0; + } + + if(StrUtil.containsIgnoreCase(number, "E")){ + // 科学计数法忽略支持,科学计数法一般用于表示非常小和非常大的数字,这类数字转换为int后精度丢失,没有意义。 + throw new NumberFormatException(StrUtil.format("Unsupported int format: [{}]", number)); + } + + if (StrUtil.startWithIgnoreCase(number, "0x")) { + // 0x04表示16进制数 + return Integer.parseInt(number.substring(2), 16); + } + + try { + return Integer.parseInt(number); + } catch (NumberFormatException e) { + return parseNumber(number).intValue(); + } + } + + /** + * 解析转换数字字符串为long型数字,规则如下: + * + *
+	 * 1、0x开头的视为16进制数字
+	 * 2、0开头的忽略开头的0
+	 * 3、空串返回0
+	 * 4、其它情况按照10进制转换
+	 * 5、.123形式返回0(按照小于0的小数对待)
+	 * 6、123.56截取小数点之前的数字,忽略小数部分
+	 * 
+ * + * @param number 数字,支持0x开头、0开头和普通十进制 + * @return long + * @since 4.1.4 + */ + public static long parseLong(String number) { + if (StrUtil.isBlank(number)) { + return 0L; + } + + if (number.startsWith("0x")) { + // 0x04表示16进制数 + return Long.parseLong(number.substring(2), 16); + } + + try { + return Long.parseLong(number); + } catch (NumberFormatException e) { + return parseNumber(number).longValue(); + } + } + + /** + * 解析转换数字字符串为long型数字,规则如下: + * + *
+	 * 1、0开头的忽略开头的0
+	 * 2、空串返回0
+	 * 3、其它情况按照10进制转换
+	 * 4、.123形式返回0.123(按照小于0的小数对待)
+	 * 
+ * + * @param number 数字,支持0x开头、0开头和普通十进制 + * @return long + * @since 5.5.5 + */ + public static float parseFloat(String number) { + if (StrUtil.isBlank(number)) { + return 0f; + } + + try { + return Float.parseFloat(number); + } catch (NumberFormatException e) { + return parseNumber(number).floatValue(); + } + } + + /** + * 解析转换数字字符串为long型数字,规则如下: + * + *
+	 * 1、0开头的忽略开头的0
+	 * 2、空串返回0
+	 * 3、其它情况按照10进制转换
+	 * 4、.123形式返回0.123(按照小于0的小数对待)
+	 * 
+ * + * @param number 数字,支持0x开头、0开头和普通十进制 + * @return long + * @since 5.5.5 + */ + public static double parseDouble(String number) { + if (StrUtil.isBlank(number)) { + return 0D; + } + + try { + return Double.parseDouble(number); + } catch (NumberFormatException e) { + return parseNumber(number).doubleValue(); + } + } + + /** + * 将指定字符串转换为{@link Number} 对象
+ * 此方法不支持科学计数法 + * + * @param numberStr Number字符串 + * @return Number对象 + * @throws NumberFormatException 包装了{@link ParseException},当给定的数字字符串无法解析时抛出 + * @since 4.1.15 + */ + public static Number parseNumber(String numberStr) throws NumberFormatException { + if (StrUtil.startWithIgnoreCase(numberStr, "0x")) { + // 0x04表示16进制数 + return Long.parseLong(numberStr.substring(2), 16); + } + + try { + final NumberFormat format = NumberFormat.getInstance(); + if (format instanceof DecimalFormat) { + // issue#1818@Github + // 当字符串数字超出double的长度时,会导致截断,此处使用BigDecimal接收 + ((DecimalFormat) format).setParseBigDecimal(true); + } + return format.parse(numberStr); + } catch (ParseException e) { + final NumberFormatException nfe = new NumberFormatException(e.getMessage()); + nfe.initCause(e); + throw nfe; + } + } + + /** + * int值转byte数组,使用大端字节序(高位字节在前,低位字节在后)
+ * 见:http://www.ruanyifeng.com/blog/2016/11/byte-order.html + * + * @param value 值 + * @return byte数组 + * @since 4.4.5 + */ + public static byte[] toBytes(int value) { + final byte[] result = new byte[4]; + + result[0] = (byte) (value >> 24); + result[1] = (byte) (value >> 16); + result[2] = (byte) (value >> 8); + result[3] = (byte) (value /* >> 0 */); + + return result; + } + + /** + * byte数组转int,使用大端字节序(高位字节在前,低位字节在后)
+ * 见:http://www.ruanyifeng.com/blog/2016/11/byte-order.html + * + * @param bytes byte数组 + * @return int + * @since 4.4.5 + */ + public static int toInt(byte[] bytes) { + return (bytes[0] & 0xff) << 24// + | (bytes[1] & 0xff) << 16// + | (bytes[2] & 0xff) << 8// + | (bytes[3] & 0xff); + } + + /** + * 以无符号字节数组的形式返回传入值。 + * + * @param value 需要转换的值 + * @return 无符号bytes + * @since 4.5.0 + */ + public static byte[] toUnsignedByteArray(BigInteger value) { + byte[] bytes = value.toByteArray(); + + if (bytes[0] == 0) { + byte[] tmp = new byte[bytes.length - 1]; + System.arraycopy(bytes, 1, tmp, 0, tmp.length); + + return tmp; + } + + return bytes; + } + + /** + * 以无符号字节数组的形式返回传入值。 + * + * @param length bytes长度 + * @param value 需要转换的值 + * @return 无符号bytes + * @since 4.5.0 + */ + public static byte[] toUnsignedByteArray(int length, BigInteger value) { + byte[] bytes = value.toByteArray(); + if (bytes.length == length) { + return bytes; + } + + int start = bytes[0] == 0 ? 1 : 0; + int count = bytes.length - start; + + if (count > length) { + throw new IllegalArgumentException("standard length exceeded for value"); + } + + byte[] tmp = new byte[length]; + System.arraycopy(bytes, start, tmp, tmp.length - count, count); + return tmp; + } + + /** + * 无符号bytes转{@link BigInteger} + * + * @param buf buf 无符号bytes + * @return {@link BigInteger} + * @since 4.5.0 + */ + public static BigInteger fromUnsignedByteArray(byte[] buf) { + return new BigInteger(1, buf); + } + + /** + * 无符号bytes转{@link BigInteger} + * + * @param buf 无符号bytes + * @param off 起始位置 + * @param length 长度 + * @return {@link BigInteger} + */ + public static BigInteger fromUnsignedByteArray(byte[] buf, int off, int length) { + byte[] mag = buf; + if (off != 0 || length != buf.length) { + mag = new byte[length]; + System.arraycopy(buf, off, mag, 0, length); + } + return new BigInteger(1, mag); + } + + /** + * 检查是否为有效的数字
+ * 检查Double和Float是否为无限大,或者Not a Number
+ * 非数字类型和Null将返回true + * + * @param number 被检查类型 + * @return 检查结果,非数字类型和Null将返回true + * @since 4.6.7 + */ + public static boolean isValidNumber(Number number) { + if (null == number) { + return false; + } + if (number instanceof Double) { + return (!((Double) number).isInfinite()) && (!((Double) number).isNaN()); + } else if (number instanceof Float) { + return (!((Float) number).isInfinite()) && (!((Float) number).isNaN()); + } + return true; + } + + /** + * 检查是否为有效的数字
+ * 检查double否为无限大,或者Not a Number(NaN)
+ * + * @param number 被检查double + * @return 检查结果 + * @since 5.7.0 + */ + public static boolean isValid(double number) { + return !(Double.isNaN(number) || Double.isInfinite(number)); + } + + /** + * 检查是否为有效的数字
+ * 检查double否为无限大,或者Not a Number(NaN)
+ * + * @param number 被检查double + * @return 检查结果 + * @since 5.7.0 + */ + public static boolean isValid(float number) { + return !(Float.isNaN(number) || Float.isInfinite(number)); + } + + /** + * 计算数学表达式的值,只支持加减乘除和取余
+ * 如: + *
+	 *   calculate("(0*1--3)-5/-4-(3*(-2.13))") -》 10.64
+	 * 
+ * + * @param expression 数学表达式 + * @return 结果 + * @since 5.7.6 + */ + public static double calculate(String expression) { + return Calculator.conversion(expression); + } + + /** + * Number值转换为double
+ * float强制转换存在精度问题,此方法避免精度丢失 + * + * @param value 被转换的float值 + * @return double值 + * @since 5.7.8 + */ + public static double toDouble(Number value) { + if (value instanceof Float) { + return Double.parseDouble(value.toString()); + } else { + return value.doubleValue(); + } + } + + /** + * 检查是否为奇数
+ * + * @param num 被判断的数值 + * @return 是否是奇数 + * @author GuoZG + * @since 5.7.17 + */ + public static boolean isOdd(int num) { + return (num & 1) == 1; + } + + /** + * 检查是否为偶数
+ * + * @param num 被判断的数值 + * @return 是否是偶数 + * @author GuoZG + * @since 5.7.17 + */ + public static boolean isEven(int num) { + return !isOdd(num); + } + + // ------------------------------------------------------------------------------------------- Private method start + private static int mathSubNode(int selectNum, int minNum) { + if (selectNum == minNum) { + return 1; + } else { + return selectNum * mathSubNode(selectNum - 1, minNum); + } + } + + private static int mathNode(int selectNum) { + if (selectNum == 0) { + return 1; + } else { + return selectNum * mathNode(selectNum - 1); + } + } + // ------------------------------------------------------------------------------------------- Private method end +} diff --git a/src/main/java/cn/hutool/core/util/ObjUtil.java b/src/main/java/cn/hutool/core/util/ObjUtil.java new file mode 100644 index 0000000..5779e27 --- /dev/null +++ b/src/main/java/cn/hutool/core/util/ObjUtil.java @@ -0,0 +1,9 @@ +package cn.hutool.core.util; + +/** + * 对象工具类,同{@link ObjectUtil}
+ * 从6.x开始,将删除ObjectUtil,而使用ObjUtil + * + */ +public class ObjUtil extends ObjectUtil{ +} diff --git a/src/main/java/cn/hutool/core/util/ObjectUtil.java b/src/main/java/cn/hutool/core/util/ObjectUtil.java new file mode 100644 index 0000000..8df636a --- /dev/null +++ b/src/main/java/cn/hutool/core/util/ObjectUtil.java @@ -0,0 +1,753 @@ +package cn.hutool.core.util; + +import cn.hutool.core.collection.IterUtil; +import cn.hutool.core.comparator.CompareUtil; +import cn.hutool.core.convert.Convert; +import cn.hutool.core.exceptions.UtilException; +import cn.hutool.core.map.MapUtil; + +import java.lang.reflect.Array; +import java.math.BigDecimal; +import java.util.*; +import java.util.function.Function; +import java.util.function.Supplier; + +/** + * 对象工具类,包括判空、克隆、序列化等操作 + * + * @author Looly + */ +public class ObjectUtil { + + /** + * 比较两个对象是否相等,此方法是 {@link #equal(Object, Object)}的别名方法。
+ * 相同的条件有两个,满足其一即可:
+ *
    + *
  1. obj1 == null && obj2 == null
  2. + *
  3. obj1.equals(obj2)
  4. + *
  5. 如果是BigDecimal比较,0 == obj1.compareTo(obj2)
  6. + *
+ * + * @param obj1 对象1 + * @param obj2 对象2 + * @return 是否相等 + * @see #equal(Object, Object) + * @since 5.4.3 + */ + public static boolean equals(Object obj1, Object obj2) { + return equal(obj1, obj2); + } + + /** + * 比较两个对象是否相等。
+ * 相同的条件有两个,满足其一即可:
+ *
    + *
  1. obj1 == null && obj2 == null
  2. + *
  3. obj1.equals(obj2)
  4. + *
  5. 如果是BigDecimal比较,0 == obj1.compareTo(obj2)
  6. + *
+ * + * @param obj1 对象1 + * @param obj2 对象2 + * @return 是否相等 + * @see Objects#equals(Object, Object) + */ + public static boolean equal(Object obj1, Object obj2) { + if (obj1 instanceof BigDecimal && obj2 instanceof BigDecimal) { + return NumberUtil.equals((BigDecimal) obj1, (BigDecimal) obj2); + } + return Objects.equals(obj1, obj2); + } + + /** + * 比较两个对象是否不相等。
+ * + * @param obj1 对象1 + * @param obj2 对象2 + * @return 是否不等 + * @since 3.0.7 + */ + public static boolean notEqual(Object obj1, Object obj2) { + return !equal(obj1, obj2); + } + + /** + * 计算对象长度,如果是字符串调用其length函数,集合类调用其size函数,数组调用其length属性,其他可遍历对象遍历计算长度
+ * 支持的类型包括: + *
    + *
  • CharSequence
  • + *
  • Map
  • + *
  • Iterator
  • + *
  • Enumeration
  • + *
  • Array
  • + *
+ * + * @param obj 被计算长度的对象 + * @return 长度 + */ + public static int length(Object obj) { + if (obj == null) { + return 0; + } + if (obj instanceof CharSequence) { + return ((CharSequence) obj).length(); + } + if (obj instanceof Collection) { + return ((Collection) obj).size(); + } + if (obj instanceof Map) { + return ((Map) obj).size(); + } + + int count; + if (obj instanceof Iterator) { + final Iterator iter = (Iterator) obj; + count = 0; + while (iter.hasNext()) { + count++; + iter.next(); + } + return count; + } + if (obj instanceof Enumeration) { + final Enumeration enumeration = (Enumeration) obj; + count = 0; + while (enumeration.hasMoreElements()) { + count++; + enumeration.nextElement(); + } + return count; + } + if (obj.getClass().isArray()) { + return Array.getLength(obj); + } + return -1; + } + + /** + * 对象中是否包含元素
+ * 支持的对象类型包括: + *
    + *
  • String
  • + *
  • Collection
  • + *
  • Map
  • + *
  • Iterator
  • + *
  • Enumeration
  • + *
  • Array
  • + *
+ * + * @param obj 对象 + * @param element 元素 + * @return 是否包含 + */ + public static boolean contains(Object obj, Object element) { + if (obj == null) { + return false; + } + if (obj instanceof String) { + if (element == null) { + return false; + } + return ((String) obj).contains(element.toString()); + } + if (obj instanceof Collection) { + return ((Collection) obj).contains(element); + } + if (obj instanceof Map) { + return ((Map) obj).containsValue(element); + } + + if (obj instanceof Iterator) { + final Iterator iter = (Iterator) obj; + while (iter.hasNext()) { + final Object o = iter.next(); + if (equal(o, element)) { + return true; + } + } + return false; + } + if (obj instanceof Enumeration) { + final Enumeration enumeration = (Enumeration) obj; + while (enumeration.hasMoreElements()) { + final Object o = enumeration.nextElement(); + if (equal(o, element)) { + return true; + } + } + return false; + } + if (obj.getClass().isArray()) { + final int len = Array.getLength(obj); + for (int i = 0; i < len; i++) { + final Object o = Array.get(obj, i); + if (equal(o, element)) { + return true; + } + } + } + return false; + } + + /** + * 检查对象是否为null
+ * 判断标准为: + * + *
+	 * 1. == null
+	 * 2. equals(null)
+	 * 
+ * + * @param obj 对象 + * @return 是否为null + */ + public static boolean isNull(Object obj) { + //noinspection ConstantConditions + return null == obj || obj.equals(null); + } + + /** + * 检查对象是否不为null + *
+	 * 1. != null
+	 * 2. not equals(null)
+	 * 
+ * + * @param obj 对象 + * @return 是否为非null + */ + public static boolean isNotNull(Object obj) { + //noinspection ConstantConditions + return null != obj && !obj.equals(null); + } + + /** + * 判断指定对象是否为空,支持: + * + *
+	 * 1. CharSequence
+	 * 2. Map
+	 * 3. Iterable
+	 * 4. Iterator
+	 * 5. Array
+	 * 
+ * + * @param obj 被判断的对象 + * @return 是否为空,如果类型不支持,返回false + * @since 4.5.7 + */ + @SuppressWarnings("rawtypes") + public static boolean isEmpty(Object obj) { + if (null == obj) { + return true; + } + + if (obj instanceof CharSequence) { + return StrUtil.isEmpty((CharSequence) obj); + } else if (obj instanceof Map) { + return MapUtil.isEmpty((Map) obj); + } else if (obj instanceof Iterable) { + return IterUtil.isEmpty((Iterable) obj); + } else if (obj instanceof Iterator) { + return IterUtil.isEmpty((Iterator) obj); + } else if (ArrayUtil.isArray(obj)) { + return ArrayUtil.isEmpty(obj); + } + + return false; + } + + /** + * 判断指定对象是否为非空,支持: + * + *
+	 * 1. CharSequence
+	 * 2. Map
+	 * 3. Iterable
+	 * 4. Iterator
+	 * 5. Array
+	 * 
+ * + * @param obj 被判断的对象 + * @return 是否为空,如果类型不支持,返回true + * @since 4.5.7 + */ + public static boolean isNotEmpty(Object obj) { + return !isEmpty(obj); + } + + /** + * 如果给定对象为{@code null}返回默认值 + * + *
+	 * ObjectUtil.defaultIfNull(null, null)      = null
+	 * ObjectUtil.defaultIfNull(null, "")        = ""
+	 * ObjectUtil.defaultIfNull(null, "zz")      = "zz"
+	 * ObjectUtil.defaultIfNull("abc", *)        = "abc"
+	 * ObjectUtil.defaultIfNull(Boolean.TRUE, *) = Boolean.TRUE
+	 * 
+ * + * @param 对象类型 + * @param object 被检查对象,可能为{@code null} + * @param defaultValue 被检查对象为{@code null}返回的默认值,可以为{@code null} + * @return 被检查对象为{@code null}返回默认值,否则返回原值 + * @since 3.0.7 + */ + public static T defaultIfNull(final T object, final T defaultValue) { + return isNull(object) ? defaultValue : object; + } + + /** + * 如果被检查对象为 {@code null}, 返回默认值(由 defaultValueSupplier 提供);否则直接返回 + * + * @param source 被检查对象 + * @param defaultValueSupplier 默认值提供者 + * @param 对象类型 + * @return 被检查对象为{@code null}返回默认值,否则返回自定义handle处理后的返回值 + * @throws NullPointerException {@code defaultValueSupplier == null} 时,抛出 + * @since 5.7.20 + */ + public static T defaultIfNull(T source, Supplier defaultValueSupplier) { + if (isNull(source)) { + return defaultValueSupplier.get(); + } + return source; + } + + /** + * 如果被检查对象为 {@code null}, 返回默认值(由 defaultValueSupplier 提供);否则直接返回 + * + * @param source 被检查对象 + * @param defaultValueSupplier 默认值提供者 + * @param 对象类型 + * @return 被检查对象为{@code null}返回默认值,否则返回自定义handle处理后的返回值 + * @throws NullPointerException {@code defaultValueSupplier == null} 时,抛出 + * @since 5.7.20 + */ + public static T defaultIfNull(T source, Function defaultValueSupplier) { + if (isNull(source)) { + return defaultValueSupplier.apply(null); + } + return source; + } + + /** + * 如果给定对象为{@code null} 返回默认值, 如果不为null 返回自定义handle处理后的返回值 + * + * @param source Object 类型对象 + * @param handle 非空时自定义的处理方法 + * @param defaultValue 默认为空的返回值 + * @param 被检查对象为{@code null}返回默认值,否则返回自定义handle处理后的返回值 + * @return 处理后的返回值 + * @since 5.4.6 + * @deprecated 当str为{@code null}时,handle使用了str相关的方法引用会导致空指针问题 + */ + @Deprecated + public static T defaultIfNull(Object source, Supplier handle, final T defaultValue) { + if (isNotNull(source)) { + return handle.get(); + } + return defaultValue; + } + + /** + * 如果给定对象为{@code null} 返回默认值, 如果不为null 返回自定义handle处理后的返回值 + * + * @param 被检查对象为{@code null}返回默认值,否则返回自定义handle处理后的返回值 + * @param 被检查的对象类型 + * @param source Object 类型对象 + * @param handle 非空时自定义的处理方法 + * @param defaultValue 默认为空的返回值 + * @return 处理后的返回值 + * @since 5.4.6 + */ + public static T defaultIfNull(R source, Function handle, final T defaultValue) { + if (isNotNull(source)) { + return handle.apply(source); + } + return defaultValue; + } + + /** + * 如果给定对象为{@code null}或者""返回默认值, 否则返回自定义handle处理后的返回值 + * + * @param str String 类型 + * @param handle 自定义的处理方法 + * @param defaultValue 默认为空的返回值 + * @param 被检查对象为{@code null}或者 ""返回默认值,否则返回自定义handle处理后的返回值 + * @return 处理后的返回值 + * @since 5.4.6 + * @deprecated 当str为{@code null}时,handle使用了str相关的方法引用会导致空指针问题 + */ + @Deprecated + public static T defaultIfEmpty(String str, Supplier handle, final T defaultValue) { + if (StrUtil.isNotEmpty(str)) { + return handle.get(); + } + return defaultValue; + } + + /** + * 如果给定对象为{@code null}或者""返回默认值, 否则返回自定义handle处理后的返回值 + * + * @param str String 类型 + * @param handle 自定义的处理方法 + * @param defaultValue 默认为空的返回值 + * @param 被检查对象为{@code null}或者 ""返回默认值,否则返回自定义handle处理后的返回值 + * @return 处理后的返回值 + * @since 5.4.6 + */ + public static T defaultIfEmpty(String str, Function handle, final T defaultValue) { + if (StrUtil.isNotEmpty(str)) { + return handle.apply(str); + } + return defaultValue; + } + + /** + * 如果给定对象为{@code null}或者 "" 返回默认值 + * + *
+	 * ObjectUtil.defaultIfEmpty(null, null)      = null
+	 * ObjectUtil.defaultIfEmpty(null, "")        = ""
+	 * ObjectUtil.defaultIfEmpty("", "zz")      = "zz"
+	 * ObjectUtil.defaultIfEmpty(" ", "zz")      = " "
+	 * ObjectUtil.defaultIfEmpty("abc", *)        = "abc"
+	 * 
+ * + * @param 对象类型(必须实现CharSequence接口) + * @param str 被检查对象,可能为{@code null} + * @param defaultValue 被检查对象为{@code null}或者 ""返回的默认值,可以为{@code null}或者 "" + * @return 被检查对象为{@code null}或者 ""返回默认值,否则返回原值 + * @since 5.0.4 + */ + public static T defaultIfEmpty(final T str, final T defaultValue) { + return StrUtil.isEmpty(str) ? defaultValue : str; + } + + /** + * 如果被检查对象为 {@code null} 或 "" 时,返回默认值(由 defaultValueSupplier 提供);否则直接返回 + * + * @param str 被检查对象 + * @param defaultValueSupplier 默认值提供者 + * @param 对象类型(必须实现CharSequence接口) + * @return 被检查对象为{@code null}返回默认值,否则返回自定义handle处理后的返回值 + * @throws NullPointerException {@code defaultValueSupplier == null} 时,抛出 + * @since 5.7.20 + */ + public static T defaultIfEmpty(T str, Supplier defaultValueSupplier) { + if (StrUtil.isEmpty(str)) { + return defaultValueSupplier.get(); + } + return str; + } + + /** + * 如果被检查对象为 {@code null} 或 "" 时,返回默认值(由 defaultValueSupplier 提供);否则直接返回 + * + * @param str 被检查对象 + * @param defaultValueSupplier 默认值提供者 + * @param 对象类型(必须实现CharSequence接口) + * @return 被检查对象为{@code null}返回默认值,否则返回自定义handle处理后的返回值 + * @throws NullPointerException {@code defaultValueSupplier == null} 时,抛出 + * @since 5.7.20 + */ + public static T defaultIfEmpty(T str, Function defaultValueSupplier) { + if (StrUtil.isEmpty(str)) { + return defaultValueSupplier.apply(null); + } + return str; + } + + /** + * 如果给定对象为{@code null}或者""或者空白符返回默认值 + * + *
+	 * ObjectUtil.defaultIfBlank(null, null)      = null
+	 * ObjectUtil.defaultIfBlank(null, "")        = ""
+	 * ObjectUtil.defaultIfBlank("", "zz")      = "zz"
+	 * ObjectUtil.defaultIfBlank(" ", "zz")      = "zz"
+	 * ObjectUtil.defaultIfBlank("abc", *)        = "abc"
+	 * 
+ * + * @param 对象类型(必须实现CharSequence接口) + * @param str 被检查对象,可能为{@code null} + * @param defaultValue 被检查对象为{@code null}或者 ""或者空白符返回的默认值,可以为{@code null}或者 ""或者空白符 + * @return 被检查对象为{@code null}或者 ""或者空白符返回默认值,否则返回原值 + * @since 5.0.4 + */ + public static T defaultIfBlank(final T str, final T defaultValue) { + return StrUtil.isBlank(str) ? defaultValue : str; + } + + /** + * 如果被检查对象为 {@code null} 或 "" 或 空白字符串时,返回默认值(由 defaultValueSupplier 提供);否则直接返回 + * + * @param str 被检查对象 + * @param defaultValueSupplier 默认值提供者 + * @param 对象类型(必须实现CharSequence接口) + * @return 被检查对象为{@code null}返回默认值,否则返回自定义handle处理后的返回值 + * @throws NullPointerException {@code defaultValueSupplier == null} 时,抛出 + * @since 5.7.20 + */ + public static T defaultIfBlank(T str, Supplier defaultValueSupplier) { + if (StrUtil.isBlank(str)) { + return defaultValueSupplier.get(); + } + return str; + } + + /** + * 如果被检查对象为 {@code null} 或 "" 或 空白字符串时,返回默认值(由 defaultValueSupplier 提供);否则直接返回 + * + * @param str 被检查对象 + * @param defaultValueSupplier 默认值提供者 + * @param 对象类型(必须实现CharSequence接口) + * @return 被检查对象为{@code null}返回默认值,否则返回自定义handle处理后的返回值 + * @throws NullPointerException {@code defaultValueSupplier == null} 时,抛出 + * @since 5.7.20 + */ + public static T defaultIfBlank(T str, Function defaultValueSupplier) { + if (StrUtil.isBlank(str)) { + return defaultValueSupplier.apply(null); + } + return str; + } + + /** + * 克隆对象
+ * 如果对象实现Cloneable接口,调用其clone方法
+ * 如果实现Serializable接口,执行深度克隆
+ * 否则返回{@code null} + * + * @param 对象类型 + * @param obj 被克隆对象 + * @return 克隆后的对象 + */ + public static T clone(T obj) { + T result = ArrayUtil.clone(obj); + if (null == result) { + if (obj instanceof Cloneable) { + result = ReflectUtil.invoke(obj, "clone"); + } else { + result = cloneByStream(obj); + } + } + return result; + } + + /** + * 返回克隆后的对象,如果克隆失败,返回原对象 + * + * @param 对象类型 + * @param obj 对象 + * @return 克隆后或原对象 + */ + public static T cloneIfPossible(final T obj) { + T clone = null; + try { + clone = clone(obj); + } catch (Exception e) { + // pass + } + return clone == null ? obj : clone; + } + + /** + * 序列化后拷贝流的方式克隆
+ * 对象必须实现Serializable接口 + * + * @param 对象类型 + * @param obj 被克隆对象 + * @return 克隆后的对象 + * @throws UtilException IO异常和ClassNotFoundException封装 + */ + public static T cloneByStream(T obj) { + return SerializeUtil.clone(obj); + } + + /** + * 序列化
+ * 对象必须实现Serializable接口 + * + * @param 对象类型 + * @param obj 要被序列化的对象 + * @return 序列化后的字节码 + */ + public static byte[] serialize(T obj) { + return SerializeUtil.serialize(obj); + } + + /** + * 反序列化
+ * 对象必须实现Serializable接口 + * + *

+ * 注意!!! 此方法不会检查反序列化安全,可能存在反序列化漏洞风险!!! + *

+ * + * @param 对象类型 + * @param bytes 反序列化的字节码 + * @return 反序列化后的对象 + */ + public static T deserialize(byte[] bytes) { + return SerializeUtil.deserialize(bytes); + } + + /** + * 是否为基本类型,包括包装类型和非包装类型 + * + * @param object 被检查对象,{@code null}返回{@code false} + * @return 是否为基本类型 + * @see ClassUtil#isBasicType(Class) + */ + public static boolean isBasicType(Object object) { + if (null == object) { + return false; + } + return ClassUtil.isBasicType(object.getClass()); + } + + /** + * 检查是否为有效的数字
+ * 检查Double和Float是否为无限大,或者Not a Number
+ * 非数字类型和Null将返回true + * + * @param obj 被检查类型 + * @return 检查结果,非数字类型和Null将返回true + */ + public static boolean isValidIfNumber(Object obj) { + if (obj instanceof Number) { + return NumberUtil.isValidNumber((Number) obj); + } + return true; + } + + /** + * {@code null}安全的对象比较,{@code null}对象排在末尾 + * + * @param 被比较对象类型 + * @param c1 对象1,可以为{@code null} + * @param c2 对象2,可以为{@code null} + * @return 比较结果,如果c1 < c2,返回数小于0,c1==c2返回0,c1 > c2 大于0 + * @see Comparator#compare(Object, Object) + * @since 3.0.7 + */ + public static > int compare(T c1, T c2) { + return CompareUtil.compare(c1, c2); + } + + /** + * {@code null}安全的对象比较 + * + * @param 被比较对象类型 + * @param c1 对象1,可以为{@code null} + * @param c2 对象2,可以为{@code null} + * @param nullGreater 当被比较对象为null时是否排在前面 + * @return 比较结果,如果c1 < c2,返回数小于0,c1==c2返回0,c1 > c2 大于0 + * @see Comparator#compare(Object, Object) + * @since 3.0.7 + */ + public static > int compare(T c1, T c2, boolean nullGreater) { + return CompareUtil.compare(c1, c2, nullGreater); + } + + /** + * 获得给定类的第一个泛型参数 + * + * @param obj 被检查的对象 + * @return {@link Class} + * @since 3.0.8 + */ + public static Class getTypeArgument(Object obj) { + return getTypeArgument(obj, 0); + } + + /** + * 获得给定类的第一个泛型参数 + * + * @param obj 被检查的对象 + * @param index 泛型类型的索引号,即第几个泛型类型 + * @return {@link Class} + * @since 3.0.8 + */ + public static Class getTypeArgument(Object obj, int index) { + return ClassUtil.getTypeArgument(obj.getClass(), index); + } + + /** + * 将Object转为String
+ * 策略为: + *
+	 *  1、null转为"null"
+	 *  2、调用Convert.toStr(Object)转换
+	 * 
+ * + * @param obj Bean对象 + * @return Bean所有字段转为Map后的字符串 + * @since 3.2.0 + */ + public static String toString(Object obj) { + if (null == obj) { + return StrUtil.NULL; + } + if (obj instanceof Map) { + return obj.toString(); + } + + return Convert.toStr(obj); + } + + /** + * 存在多少个{@code null}或空对象,通过{@link ObjectUtil#isEmpty(Object)} 判断元素 + * + * @param objs 被检查的对象,一个或者多个 + * @return 存在{@code null}的数量 + */ + public static int emptyCount(Object... objs) { + return ArrayUtil.emptyCount(objs); + } + + /** + * 是否存在{@code null}对象,通过{@link ObjectUtil#isNull(Object)} 判断元素 + * + * @param objs 被检查对象 + * @return 是否存在 + * @see ArrayUtil#hasNull(Object[]) + * @since 5.5.3 + */ + public static boolean hasNull(Object... objs) { + return ArrayUtil.hasNull(objs); + } + + /** + * 是否存在{@code null}或空对象,通过{@link ObjectUtil#isEmpty(Object)} 判断元素 + * + * @param objs 被检查对象 + * @return 是否存在 + * @see ArrayUtil#hasEmpty(Object...) + */ + public static boolean hasEmpty(Object... objs) { + return ArrayUtil.hasEmpty(objs); + } + + /** + * 是否全都为{@code null}或空对象,通过{@link ObjectUtil#isEmpty(Object)} 判断元素 + * + * @param objs 被检查的对象,一个或者多个 + * @return 是否都为空 + */ + public static boolean isAllEmpty(Object... objs) { + return ArrayUtil.isAllEmpty(objs); + } + + /** + * 是否全都不为{@code null}或空对象,通过{@link ObjectUtil#isEmpty(Object)} 判断元素 + * + * @param objs 被检查的对象,一个或者多个 + * @return 是否都不为空 + */ + public static boolean isAllNotEmpty(Object... objs) { + return ArrayUtil.isAllNotEmpty(objs); + } +} diff --git a/src/main/java/cn/hutool/core/util/PageUtil.java b/src/main/java/cn/hutool/core/util/PageUtil.java new file mode 100644 index 0000000..d62d881 --- /dev/null +++ b/src/main/java/cn/hutool/core/util/PageUtil.java @@ -0,0 +1,274 @@ +package cn.hutool.core.util; + +import cn.hutool.core.lang.DefaultSegment; +import cn.hutool.core.lang.Segment; + +/** + * 分页工具类 + * + * @author xiaoleilu + */ +public class PageUtil { + + private static int firstPageNo = 0; + + /** + * 获得首页的页码,可以为0或者1 + * + * @return 首页页码 + */ + public static int getFirstPageNo() { + return firstPageNo; + } + + /** + * 设置首页页码,可以为0或者1 + * + *
+	 *     当设置为0时,页码0表示第一页,开始位置为0
+	 *     当设置为1时,页码1表示第一页,开始位置为0
+	 * 
+ * + * @param customFirstPageNo 自定义的首页页码,为0或者1 + */ + synchronized public static void setFirstPageNo(int customFirstPageNo) { + firstPageNo = customFirstPageNo; + } + + /** + * 设置首页页码为1 + * + *
+	 *     当设置为1时,页码1表示第一页,开始位置为0
+	 * 
+ */ + public static void setOneAsFirstPageNo() { + setFirstPageNo(1); + } + + /** + * 将页数和每页条目数转换为开始位置
+ * 此方法用于不包括结束位置的分页方法
+ * 例如: + * + *
+	 * 页码:0,每页10 =》 0
+	 * 页码:1,每页10 =》 10
+	 * ……
+	 * 
+ * + *

+ * 当{@link #setFirstPageNo(int)}设置为1时: + *

+	 * 页码:1,每页10 =》 0
+	 * 页码:2,每页10 =》 10
+	 * ……
+	 * 
+ * + * @param pageNo 页码(从0计数) + * @param pageSize 每页条目数 + * @return 开始位置 + */ + public static int getStart(int pageNo, int pageSize) { + if (pageNo < firstPageNo) { + pageNo = firstPageNo; + } + + if (pageSize < 1) { + pageSize = 0; + } + + return (pageNo - firstPageNo) * pageSize; + } + + /** + * 将页数和每页条目数转换为结束位置
+ * 此方法用于不包括结束位置的分页方法
+ * 例如: + * + *
+	 * 页码:0,每页10 =》 9
+	 * 页码:1,每页10 =》 19
+	 * ……
+	 * 
+ * + *

+ * 当{@link #setFirstPageNo(int)}设置为1时: + *

+	 * 页码:1,每页10 =》 9
+	 * 页码:2,每页10 =》 19
+	 * ……
+	 * 
+ * + * @param pageNo 页码(从0计数) + * @param pageSize 每页条目数 + * @return 开始位置 + * @since 5.2.5 + */ + public static int getEnd(int pageNo, int pageSize) { + final int start = getStart(pageNo, pageSize); + return getEndByStart(start, pageSize); + } + + /** + * 将页数和每页条目数转换为开始位置和结束位置
+ * 此方法用于包括结束位置的分页方法
+ * 例如: + * + *
+	 * 页码:0,每页10 =》 [0, 10]
+	 * 页码:1,每页10 =》 [10, 20]
+	 * ……
+	 * 
+ * + *

+ * 当{@link #setFirstPageNo(int)}设置为1时: + *

+	 * 页码:1,每页10 =》 [0, 10]
+	 * 页码:2,每页10 =》 [10, 20]
+	 * ……
+	 * 
+ * + * @param pageNo 页码(从0计数) + * @param pageSize 每页条目数 + * @return 第一个数为开始位置,第二个数为结束位置 + */ + public static int[] transToStartEnd(int pageNo, int pageSize) { + final int start = getStart(pageNo, pageSize); + return new int[]{start, getEndByStart(start, pageSize)}; + } + + /** + * 将页数和每页条目数转换为开始位置和结束位置
+ * 此方法用于包括结束位置的分页方法
+ * 例如: + * + *
+	 * 页码:0,每页10 =》 [0, 10]
+	 * 页码:1,每页10 =》 [10, 20]
+	 * ……
+	 * 
+ * + *

+ * 当{@link #setFirstPageNo(int)}设置为1时: + *

+	 * 页码:1,每页10 =》 [0, 10]
+	 * 页码:2,每页10 =》 [10, 20]
+	 * ……
+	 * 
+ * + * @param pageNo 页码(从0计数) + * @param pageSize 每页条目数 + * @return {@link Segment} + * @since 5.5.3 + */ + public static Segment toSegment(int pageNo, int pageSize) { + final int[] startEnd = transToStartEnd(pageNo, pageSize); + return new DefaultSegment<>(startEnd[0], startEnd[1]); + } + + /** + * 根据总数计算总页数 + * + * @param totalCount 总数 + * @param pageSize 每页数 + * @return 总页数 + */ + public static int totalPage(int totalCount, int pageSize) { + return totalPage((long) totalCount,pageSize); + } + + /** + * 根据总数计算总页数 + * + * @param totalCount 总数 + * @param pageSize 每页数 + * @return 总页数 + * @since 5.8.5 + */ + public static int totalPage(long totalCount, int pageSize) { + if (pageSize == 0) { + return 0; + } + return Math.toIntExact(totalCount % pageSize == 0 ? (totalCount / pageSize) : (totalCount / pageSize + 1)); + } + + /** + * 分页彩虹算法
+ * 来自: + * https://github.com/iceroot/iceroot/blob/master/src/main/java/com/icexxx/util/IceUtil.java
+ * 通过传入的信息,生成一个分页列表显示 + * + * @param pageNo 当前页 + * @param totalPage 总页数 + * @param displayCount 每屏展示的页数 + * @return 分页条 + */ + public static int[] rainbow(int pageNo, int totalPage, int displayCount) { + // displayCount % 2 + boolean isEven = (displayCount & 1) == 0; + int left = displayCount >> 1; + int right = displayCount >> 1; + + int length = displayCount; + if (isEven) { + right++; + } + if (totalPage < displayCount) { + length = totalPage; + } + int[] result = new int[length]; + if (totalPage >= displayCount) { + if (pageNo <= left) { + for (int i = 0; i < result.length; i++) { + result[i] = i + 1; + } + } else if (pageNo > totalPage - right) { + for (int i = 0; i < result.length; i++) { + result[i] = i + totalPage - displayCount + 1; + } + } else { + for (int i = 0; i < result.length; i++) { + result[i] = i + pageNo - left + (isEven ? 1 : 0); + } + } + } else { + for (int i = 0; i < result.length; i++) { + result[i] = i + 1; + } + } + return result; + + } + + /** + * 分页彩虹算法(默认展示10页)
+ * 来自: + * https://github.com/iceroot/iceroot/blob/master/src/main/java/com/icexxx/util/IceUtil.java + * + * @param currentPage 当前页 + * @param pageCount 总页数 + * @return 分页条 + */ + public static int[] rainbow(int currentPage, int pageCount) { + return rainbow(currentPage, pageCount, 10); + } + + //------------------------------------------------------------------------- Private method start + + /** + * 根据起始位置获取结束位置 + * + * @param start 起始位置 + * @param pageSize 每页条目数 + * @return 结束位置 + */ + private static int getEndByStart(int start, int pageSize) { + if (pageSize < 1) { + pageSize = 0; + } + return start + pageSize; + } + + //------------------------------------------------------------------------- Private method end +} diff --git a/src/main/java/cn/hutool/core/util/PhoneUtil.java b/src/main/java/cn/hutool/core/util/PhoneUtil.java new file mode 100644 index 0000000..5386dc8 --- /dev/null +++ b/src/main/java/cn/hutool/core/util/PhoneUtil.java @@ -0,0 +1,188 @@ +package cn.hutool.core.util; + +import cn.hutool.core.lang.PatternPool; +import cn.hutool.core.lang.Validator; + + +/** + * 电话号码工具类,包括: + *
    + *
  • 手机号码
  • + *
  • 400、800号码
  • + *
  • 座机号码
  • + *
+ * + * @author dahuoyzs + * @since 5.3.11 + */ +public class PhoneUtil { + + /** + * 验证是否为手机号码(中国大陆) + * + * @param value 值 + * @return 是否为手机号码(中国大陆) + * @since 5.3.11 + */ + public static boolean isMobile(CharSequence value) { + return Validator.isMatchRegex(PatternPool.MOBILE, value); + } + + /** + * 验证是否为手机号码(中国香港) + * @param value 手机号码 + * @return 是否为中国香港手机号码 + * @since 5.6.3 + * @author dazer, ourslook + */ + public static boolean isMobileHk(CharSequence value) { + return Validator.isMatchRegex(PatternPool.MOBILE_HK, value); + } + + /** + * 验证是否为手机号码(中国台湾) + * @param value 手机号码 + * @return 是否为中国台湾手机号码 + * @since 5.6.6 + * @author ihao + */ + public static boolean isMobileTw(CharSequence value) { + return Validator.isMatchRegex(PatternPool.MOBILE_TW, value); + } + + /** + * 验证是否为手机号码(中国澳门) + * @param value 手机号码 + * @return 是否为中国澳门手机号码 + * @since 5.6.6 + * @author ihao + */ + public static boolean isMobileMo(CharSequence value) { + return Validator.isMatchRegex(PatternPool.MOBILE_MO, value); + } + + /** + * 验证是否为座机号码(中国大陆) + * + * @param value 值 + * @return 是否为座机号码(中国大陆) + * @since 5.3.11 + */ + public static boolean isTel(CharSequence value) { + return Validator.isMatchRegex(PatternPool.TEL, value); + } + + /** + * 验证是否为座机号码(中国大陆)+ 400 + 800 + * + * @param value 值 + * @return 是否为座机号码(中国大陆) + * @since 5.6.3 + * @author dazer, ourslook + */ + public static boolean isTel400800(CharSequence value) { + return Validator.isMatchRegex(PatternPool.TEL_400_800, value); + } + + /** + * 验证是否为座机号码+手机号码(CharUtil中国)+ 400 + 800电话 + 手机号号码(中国香港) + * + * @param value 值 + * @return 是否为座机号码+手机号码(中国大陆)+手机号码(中国香港)+手机号码(中国台湾)+手机号码(中国澳门) + * @since 5.3.11 + */ + public static boolean isPhone(CharSequence value) { + return isMobile(value) || isTel400800(value) || isMobileHk(value) || isMobileTw(value) || isMobileMo(value); + } + + /** + * 隐藏手机号前7位 替换字符为"*" + * 栗子 + * + * @param phone 手机号码 + * @return 替换后的字符串 + * @since 5.3.11 + */ + public static CharSequence hideBefore(CharSequence phone) { + return StrUtil.hide(phone, 0, 7); + } + + /** + * 隐藏手机号中间4位 替换字符为"*" + * + * @param phone 手机号码 + * @return 替换后的字符串 + * @since 5.3.11 + */ + public static CharSequence hideBetween(CharSequence phone) { + return StrUtil.hide(phone, 3, 7); + } + + /** + * 隐藏手机号最后4位 替换字符为"*" + * + * @param phone 手机号码 + * @return 替换后的字符串 + * @since 5.3.11 + */ + public static CharSequence hideAfter(CharSequence phone) { + return StrUtil.hide(phone, 7, 11); + } + + /** + * 获取手机号前3位 + * + * @param phone 手机号码 + * @return 手机号前3位 + * @since 5.3.11 + */ + public static CharSequence subBefore(CharSequence phone) { + return StrUtil.sub(phone, 0, 3); + } + + /** + * 获取手机号中间4位 + * + * @param phone 手机号码 + * @return 手机号中间4位 + * @since 5.3.11 + */ + public static CharSequence subBetween(CharSequence phone) { + return StrUtil.sub(phone, 3, 7); + } + + /** + * 获取手机号后4位 + * + * @param phone 手机号码 + * @return 手机号后4位 + * @since 5.3.11 + */ + public static CharSequence subAfter(CharSequence phone) { + return StrUtil.sub(phone, 7, 11); + } + + /** + * 获取固话号码中的区号 + * + * @param value 完整的固话号码 + * @return 固话号码的区号部分 + * @since 5.7.7 + */ + public static CharSequence subTelBefore(CharSequence value) + { + return ReUtil.getGroup1(PatternPool.TEL, value); + } + + /** + * 获取固话号码中的号码 + * + * @param value 完整的固话号码 + * @return 固话号码的号码部分 + * @since 5.7.7 + */ + public static CharSequence subTelAfter(CharSequence value) + { + return ReUtil.get(PatternPool.TEL, value, 2); + } +} diff --git a/src/main/java/cn/hutool/core/util/PrimitiveArrayUtil.java b/src/main/java/cn/hutool/core/util/PrimitiveArrayUtil.java new file mode 100644 index 0000000..13700a9 --- /dev/null +++ b/src/main/java/cn/hutool/core/util/PrimitiveArrayUtil.java @@ -0,0 +1,3217 @@ +package cn.hutool.core.util; + +import java.lang.reflect.Array; +import java.util.Arrays; +import java.util.Random; + +/** + * 原始类型数组工具类 + * + * @author looly + * @since 5.5.2 + */ +public class PrimitiveArrayUtil { + /** + * 数组中元素未找到的下标,值为-1 + */ + public static final int INDEX_NOT_FOUND = -1; + + // ---------------------------------------------------------------------- isEmpty + + /** + * 数组是否为空 + * + * @param array 数组 + * @return 是否为空 + */ + public static boolean isEmpty(long[] array) { + return array == null || array.length == 0; + } + + /** + * 数组是否为空 + * + * @param array 数组 + * @return 是否为空 + */ + public static boolean isEmpty(int[] array) { + return array == null || array.length == 0; + } + + /** + * 数组是否为空 + * + * @param array 数组 + * @return 是否为空 + */ + public static boolean isEmpty(short[] array) { + return array == null || array.length == 0; + } + + /** + * 数组是否为空 + * + * @param array 数组 + * @return 是否为空 + */ + public static boolean isEmpty(char[] array) { + return array == null || array.length == 0; + } + + /** + * 数组是否为空 + * + * @param array 数组 + * @return 是否为空 + */ + public static boolean isEmpty(byte[] array) { + return array == null || array.length == 0; + } + + /** + * 数组是否为空 + * + * @param array 数组 + * @return 是否为空 + */ + public static boolean isEmpty(double[] array) { + return array == null || array.length == 0; + } + + /** + * 数组是否为空 + * + * @param array 数组 + * @return 是否为空 + */ + public static boolean isEmpty(float[] array) { + return array == null || array.length == 0; + } + + /** + * 数组是否为空 + * + * @param array 数组 + * @return 是否为空 + */ + public static boolean isEmpty(boolean[] array) { + return array == null || array.length == 0; + } + + // ---------------------------------------------------------------------- isNotEmpty + + /** + * 数组是否为非空 + * + * @param array 数组 + * @return 是否为非空 + */ + public static boolean isNotEmpty(long[] array) { + return !isEmpty(array); + } + + /** + * 数组是否为非空 + * + * @param array 数组 + * @return 是否为非空 + */ + public static boolean isNotEmpty(int[] array) { + return !isEmpty(array); + } + + /** + * 数组是否为非空 + * + * @param array 数组 + * @return 是否为非空 + */ + public static boolean isNotEmpty(short[] array) { + return !isEmpty(array); + } + + /** + * 数组是否为非空 + * + * @param array 数组 + * @return 是否为非空 + */ + public static boolean isNotEmpty(char[] array) { + return !isEmpty(array); + } + + /** + * 数组是否为非空 + * + * @param array 数组 + * @return 是否为非空 + */ + public static boolean isNotEmpty(byte[] array) { + return !isEmpty(array); + } + + /** + * 数组是否为非空 + * + * @param array 数组 + * @return 是否为非空 + */ + public static boolean isNotEmpty(double[] array) { + return !isEmpty(array); + } + + /** + * 数组是否为非空 + * + * @param array 数组 + * @return 是否为非空 + */ + public static boolean isNotEmpty(float[] array) { + return !isEmpty(array); + } + + /** + * 数组是否为非空 + * + * @param array 数组 + * @return 是否为非空 + */ + public static boolean isNotEmpty(boolean[] array) { + return !isEmpty(array); + } + + // ---------------------------------------------------------------------- resize + + /** + * 生成一个新的重新设置大小的数组
+ * 调整大小后拷贝原数组到新数组下。扩大则占位前N个位置,其它位置补充0,缩小则截断 + * + * @param bytes 原数组 + * @param newSize 新的数组大小 + * @return 调整后的新数组 + * @since 4.6.7 + */ + public static byte[] resize(byte[] bytes, int newSize) { + if (newSize < 0) { + return bytes; + } + final byte[] newArray = new byte[newSize]; + if (newSize > 0 && isNotEmpty(bytes)) { + System.arraycopy(bytes, 0, newArray, 0, Math.min(bytes.length, newSize)); + } + return newArray; + } + + // ---------------------------------------------------------------------- addAll + + /** + * 将多个数组合并在一起
+ * 忽略null的数组 + * + * @param arrays 数组集合 + * @return 合并后的数组 + * @since 4.6.9 + */ + public static byte[] addAll(byte[]... arrays) { + if (arrays.length == 1) { + return arrays[0]; + } + + // 计算总长度 + int length = 0; + for (byte[] array : arrays) { + if (null != array) { + length += array.length; + } + } + + final byte[] result = new byte[length]; + length = 0; + for (byte[] array : arrays) { + if (null != array) { + System.arraycopy(array, 0, result, length, array.length); + length += array.length; + } + } + return result; + } + + /** + * 将多个数组合并在一起
+ * 忽略null的数组 + * + * @param arrays 数组集合 + * @return 合并后的数组 + * @since 4.6.9 + */ + public static int[] addAll(int[]... arrays) { + if (arrays.length == 1) { + return arrays[0]; + } + + // 计算总长度 + int length = 0; + for (int[] array : arrays) { + if (null != array) { + length += array.length; + } + } + + final int[] result = new int[length]; + length = 0; + for (int[] array : arrays) { + if (null != array) { + System.arraycopy(array, 0, result, length, array.length); + length += array.length; + } + } + return result; + } + + /** + * 将多个数组合并在一起
+ * 忽略null的数组 + * + * @param arrays 数组集合 + * @return 合并后的数组 + * @since 4.6.9 + */ + public static long[] addAll(long[]... arrays) { + if (arrays.length == 1) { + return arrays[0]; + } + + // 计算总长度 + int length = 0; + for (long[] array : arrays) { + if (null != array) { + length += array.length; + } + } + + final long[] result = new long[length]; + length = 0; + for (long[] array : arrays) { + if (null != array) { + System.arraycopy(array, 0, result, length, array.length); + length += array.length; + } + } + return result; + } + + /** + * 将多个数组合并在一起
+ * 忽略null的数组 + * + * @param arrays 数组集合 + * @return 合并后的数组 + * @since 4.6.9 + */ + public static double[] addAll(double[]... arrays) { + if (arrays.length == 1) { + return arrays[0]; + } + + // 计算总长度 + int length = 0; + for (double[] array : arrays) { + if (null != array) { + length += array.length; + } + } + + final double[] result = new double[length]; + length = 0; + for (double[] array : arrays) { + if (null != array) { + System.arraycopy(array, 0, result, length, array.length); + length += array.length; + } + } + return result; + } + + /** + * 将多个数组合并在一起
+ * 忽略null的数组 + * + * @param arrays 数组集合 + * @return 合并后的数组 + * @since 4.6.9 + */ + public static float[] addAll(float[]... arrays) { + if (arrays.length == 1) { + return arrays[0]; + } + + // 计算总长度 + int length = 0; + for (float[] array : arrays) { + if (null != array) { + length += array.length; + } + } + + final float[] result = new float[length]; + length = 0; + for (float[] array : arrays) { + if (null != array) { + System.arraycopy(array, 0, result, length, array.length); + length += array.length; + } + } + return result; + } + + /** + * 将多个数组合并在一起
+ * 忽略null的数组 + * + * @param arrays 数组集合 + * @return 合并后的数组 + * @since 4.6.9 + */ + public static char[] addAll(char[]... arrays) { + if (arrays.length == 1) { + return arrays[0]; + } + + // 计算总长度 + int length = 0; + for (char[] array : arrays) { + if (null != array) { + length += array.length; + } + } + + final char[] result = new char[length]; + length = 0; + for (char[] array : arrays) { + if (null != array) { + System.arraycopy(array, 0, result, length, array.length); + length += array.length; + } + } + return result; + } + + /** + * 将多个数组合并在一起
+ * 忽略null的数组 + * + * @param arrays 数组集合 + * @return 合并后的数组 + * @since 4.6.9 + */ + public static boolean[] addAll(boolean[]... arrays) { + if (arrays.length == 1) { + return arrays[0]; + } + + // 计算总长度 + int length = 0; + for (boolean[] array : arrays) { + if (null != array) { + length += array.length; + } + } + + final boolean[] result = new boolean[length]; + length = 0; + for (boolean[] array : arrays) { + if (null != array) { + System.arraycopy(array, 0, result, length, array.length); + length += array.length; + } + } + return result; + } + + /** + * 将多个数组合并在一起
+ * 忽略null的数组 + * + * @param arrays 数组集合 + * @return 合并后的数组 + * @since 4.6.9 + */ + public static short[] addAll(short[]... arrays) { + if (arrays.length == 1) { + return arrays[0]; + } + + // 计算总长度 + int length = 0; + for (short[] array : arrays) { + if (null != array) { + length += array.length; + } + } + + final short[] result = new short[length]; + length = 0; + for (short[] array : arrays) { + if (null != array) { + System.arraycopy(array, 0, result, length, array.length); + length += array.length; + } + } + return result; + } + + // ---------------------------------------------------------------------- range + + /** + * 生成一个从0开始的数字列表
+ * + * @param excludedEnd 结束的数字(不包含) + * @return 数字列表 + */ + public static int[] range(int excludedEnd) { + return range(0, excludedEnd, 1); + } + + /** + * 生成一个数字列表
+ * 自动判定正序反序 + * + * @param includedStart 开始的数字(包含) + * @param excludedEnd 结束的数字(不包含) + * @return 数字列表 + */ + public static int[] range(int includedStart, int excludedEnd) { + return range(includedStart, excludedEnd, 1); + } + + /** + * 生成一个数字列表
+ * 自动判定正序反序 + * + * @param includedStart 开始的数字(包含) + * @param excludedEnd 结束的数字(不包含) + * @param step 步进 + * @return 数字列表 + */ + public static int[] range(int includedStart, int excludedEnd, int step) { + if (includedStart > excludedEnd) { + int tmp = includedStart; + includedStart = excludedEnd; + excludedEnd = tmp; + } + + if (step <= 0) { + step = 1; + } + + int deviation = excludedEnd - includedStart; + int length = deviation / step; + if (deviation % step != 0) { + length += 1; + } + int[] range = new int[length]; + for (int i = 0; i < length; i++) { + range[i] = includedStart; + includedStart += step; + } + return range; + } + + // ---------------------------------------------------------------------- split + + /** + * 拆分byte数组为几个等份(最后一份按照剩余长度分配空间) + * + * @param array 数组 + * @param len 每个小节的长度 + * @return 拆分后的数组 + */ + public static byte[][] split(byte[] array, int len) { + int amount = array.length / len; + final int remainder = array.length % len; + if (remainder != 0) { + ++amount; + } + final byte[][] arrays = new byte[amount][]; + byte[] arr; + for (int i = 0; i < amount; i++) { + if (i == amount - 1 && remainder != 0) { + // 有剩余,按照实际长度创建 + arr = new byte[remainder]; + System.arraycopy(array, i * len, arr, 0, remainder); + } else { + arr = new byte[len]; + System.arraycopy(array, i * len, arr, 0, len); + } + arrays[i] = arr; + } + return arrays; + } + + // ---------------------------------------------------------------------- indexOf、LastIndexOf、contains + + /** + * 返回数组中指定元素所在位置,未找到返回{@link #INDEX_NOT_FOUND} + * + * @param array 数组 + * @param value 被检查的元素 + * @return 数组中指定元素所在位置,未找到返回{@link #INDEX_NOT_FOUND} + * @since 3.0.7 + */ + public static int indexOf(long[] array, long value) { + if (null != array) { + for (int i = 0; i < array.length; i++) { + if (value == array[i]) { + return i; + } + } + } + return INDEX_NOT_FOUND; + } + + /** + * 返回数组中指定元素所在最后的位置,未找到返回{@link #INDEX_NOT_FOUND} + * + * @param array 数组 + * @param value 被检查的元素 + * @return 数组中指定元素所在位置,未找到返回{@link #INDEX_NOT_FOUND} + * @since 3.0.7 + */ + public static int lastIndexOf(long[] array, long value) { + if (null != array) { + for (int i = array.length - 1; i >= 0; i--) { + if (value == array[i]) { + return i; + } + } + } + return INDEX_NOT_FOUND; + } + + /** + * 数组中是否包含元素 + * + * @param array 数组 + * @param value 被检查的元素 + * @return 是否包含 + * @since 3.0.7 + */ + public static boolean contains(long[] array, long value) { + return indexOf(array, value) > INDEX_NOT_FOUND; + } + + /** + * 返回数组中指定元素所在位置,未找到返回{@link #INDEX_NOT_FOUND} + * + * @param array 数组 + * @param value 被检查的元素 + * @return 数组中指定元素所在位置,未找到返回{@link #INDEX_NOT_FOUND} + * @since 3.0.7 + */ + public static int indexOf(int[] array, int value) { + if (null != array) { + for (int i = 0; i < array.length; i++) { + if (value == array[i]) { + return i; + } + } + } + return INDEX_NOT_FOUND; + } + + /** + * 返回数组中指定元素所在最后的位置,未找到返回{@link #INDEX_NOT_FOUND} + * + * @param array 数组 + * @param value 被检查的元素 + * @return 数组中指定元素所在位置,未找到返回{@link #INDEX_NOT_FOUND} + * @since 3.0.7 + */ + public static int lastIndexOf(int[] array, int value) { + if (null != array) { + for (int i = array.length - 1; i >= 0; i--) { + if (value == array[i]) { + return i; + } + } + } + return INDEX_NOT_FOUND; + } + + /** + * 数组中是否包含元素 + * + * @param array 数组 + * @param value 被检查的元素 + * @return 是否包含 + * @since 3.0.7 + */ + public static boolean contains(int[] array, int value) { + return indexOf(array, value) > INDEX_NOT_FOUND; + } + + /** + * 返回数组中指定元素所在位置,未找到返回{@link #INDEX_NOT_FOUND} + * + * @param array 数组 + * @param value 被检查的元素 + * @return 数组中指定元素所在位置,未找到返回{@link #INDEX_NOT_FOUND} + * @since 3.0.7 + */ + public static int indexOf(short[] array, short value) { + if (null != array) { + for (int i = 0; i < array.length; i++) { + if (value == array[i]) { + return i; + } + } + } + return INDEX_NOT_FOUND; + } + + /** + * 返回数组中指定元素所在最后的位置,未找到返回{@link #INDEX_NOT_FOUND} + * + * @param array 数组 + * @param value 被检查的元素 + * @return 数组中指定元素所在位置,未找到返回{@link #INDEX_NOT_FOUND} + * @since 3.0.7 + */ + public static int lastIndexOf(short[] array, short value) { + if (null != array) { + for (int i = array.length - 1; i >= 0; i--) { + if (value == array[i]) { + return i; + } + } + } + return INDEX_NOT_FOUND; + } + + /** + * 数组中是否包含元素 + * + * @param array 数组 + * @param value 被检查的元素 + * @return 是否包含 + * @since 3.0.7 + */ + public static boolean contains(short[] array, short value) { + return indexOf(array, value) > INDEX_NOT_FOUND; + } + + /** + * 返回数组中指定元素所在位置,未找到返回{@link #INDEX_NOT_FOUND} + * + * @param array 数组 + * @param value 被检查的元素 + * @return 数组中指定元素所在位置,未找到返回{@link #INDEX_NOT_FOUND} + * @since 3.0.7 + */ + public static int indexOf(char[] array, char value) { + if (null != array) { + for (int i = 0; i < array.length; i++) { + if (value == array[i]) { + return i; + } + } + } + return INDEX_NOT_FOUND; + } + + /** + * 返回数组中指定元素所在最后的位置,未找到返回{@link #INDEX_NOT_FOUND} + * + * @param array 数组 + * @param value 被检查的元素 + * @return 数组中指定元素所在位置,未找到返回{@link #INDEX_NOT_FOUND} + * @since 3.0.7 + */ + public static int lastIndexOf(char[] array, char value) { + if (null != array) { + for (int i = array.length - 1; i >= 0; i--) { + if (value == array[i]) { + return i; + } + } + } + return INDEX_NOT_FOUND; + } + + /** + * 数组中是否包含元素 + * + * @param array 数组 + * @param value 被检查的元素 + * @return 是否包含 + * @since 3.0.7 + */ + public static boolean contains(char[] array, char value) { + return indexOf(array, value) > INDEX_NOT_FOUND; + } + + /** + * 返回数组中指定元素所在位置,未找到返回{@link #INDEX_NOT_FOUND} + * + * @param array 数组 + * @param value 被检查的元素 + * @return 数组中指定元素所在位置,未找到返回{@link #INDEX_NOT_FOUND} + * @since 3.0.7 + */ + public static int indexOf(byte[] array, byte value) { + if (null != array) { + for (int i = 0; i < array.length; i++) { + if (value == array[i]) { + return i; + } + } + } + return INDEX_NOT_FOUND; + } + + /** + * 返回数组中指定元素所在最后的位置,未找到返回{@link #INDEX_NOT_FOUND} + * + * @param array 数组 + * @param value 被检查的元素 + * @return 数组中指定元素所在位置,未找到返回{@link #INDEX_NOT_FOUND} + * @since 3.0.7 + */ + public static int lastIndexOf(byte[] array, byte value) { + if (null != array) { + for (int i = array.length - 1; i >= 0; i--) { + if (value == array[i]) { + return i; + } + } + } + return INDEX_NOT_FOUND; + } + + /** + * 数组中是否包含元素 + * + * @param array 数组 + * @param value 被检查的元素 + * @return 是否包含 + * @since 3.0.7 + */ + public static boolean contains(byte[] array, byte value) { + return indexOf(array, value) > INDEX_NOT_FOUND; + } + + /** + * 返回数组中指定元素所在位置,未找到返回{@link #INDEX_NOT_FOUND} + * + * @param array 数组 + * @param value 被检查的元素 + * @return 数组中指定元素所在位置,未找到返回{@link #INDEX_NOT_FOUND} + * @since 3.0.7 + */ + public static int indexOf(double[] array, double value) { + if (null != array) { + for (int i = 0; i < array.length; i++) { + if (NumberUtil.equals(value, array[i])) { + return i; + } + } + } + return INDEX_NOT_FOUND; + } + + /** + * 返回数组中指定元素所在最后的位置,未找到返回{@link #INDEX_NOT_FOUND} + * + * @param array 数组 + * @param value 被检查的元素 + * @return 数组中指定元素所在位置,未找到返回{@link #INDEX_NOT_FOUND} + * @since 3.0.7 + */ + public static int lastIndexOf(double[] array, double value) { + if (null != array) { + for (int i = array.length - 1; i >= 0; i--) { + if (NumberUtil.equals(value, array[i])) { + return i; + } + } + } + return INDEX_NOT_FOUND; + } + + /** + * 数组中是否包含元素 + * + * @param array 数组 + * @param value 被检查的元素 + * @return 是否包含 + * @since 3.0.7 + */ + public static boolean contains(double[] array, double value) { + return indexOf(array, value) > INDEX_NOT_FOUND; + } + + /** + * 返回数组中指定元素所在位置,未找到返回{@link #INDEX_NOT_FOUND} + * + * @param array 数组 + * @param value 被检查的元素 + * @return 数组中指定元素所在位置,未找到返回{@link #INDEX_NOT_FOUND} + * @since 3.0.7 + */ + public static int indexOf(float[] array, float value) { + if (null != array) { + for (int i = 0; i < array.length; i++) { + if (NumberUtil.equals(value, array[i])) { + return i; + } + } + } + return INDEX_NOT_FOUND; + } + + /** + * 返回数组中指定元素所在最后的位置,未找到返回{@link #INDEX_NOT_FOUND} + * + * @param array 数组 + * @param value 被检查的元素 + * @return 数组中指定元素所在位置,未找到返回{@link #INDEX_NOT_FOUND} + * @since 3.0.7 + */ + public static int lastIndexOf(float[] array, float value) { + if (null != array) { + for (int i = array.length - 1; i >= 0; i--) { + if (NumberUtil.equals(value, array[i])) { + return i; + } + } + } + return INDEX_NOT_FOUND; + } + + /** + * 数组中是否包含元素 + * + * @param array 数组 + * @param value 被检查的元素 + * @return 是否包含 + * @since 3.0.7 + */ + public static boolean contains(float[] array, float value) { + return indexOf(array, value) > INDEX_NOT_FOUND; + } + + /** + * 返回数组中指定元素所在位置,未找到返回{@link #INDEX_NOT_FOUND} + * + * @param array 数组 + * @param value 被检查的元素 + * @return 数组中指定元素所在位置,未找到返回{@link #INDEX_NOT_FOUND} + * @since 3.0.7 + */ + public static int indexOf(boolean[] array, boolean value) { + if (null != array) { + for (int i = 0; i < array.length; i++) { + if (value == array[i]) { + return i; + } + } + } + return INDEX_NOT_FOUND; + } + + /** + * 返回数组中指定元素所在最后的位置,未找到返回{@link #INDEX_NOT_FOUND} + * + * @param array 数组 + * @param value 被检查的元素 + * @return 数组中指定元素所在位置,未找到返回{@link #INDEX_NOT_FOUND} + * @since 3.0.7 + */ + public static int lastIndexOf(boolean[] array, boolean value) { + if (null != array) { + for (int i = array.length - 1; i >= 0; i--) { + if (value == array[i]) { + return i; + } + } + } + return INDEX_NOT_FOUND; + } + + /** + * 数组中是否包含元素 + * + * @param array 数组 + * @param value 被检查的元素 + * @return 是否包含 + * @since 3.0.7 + */ + public static boolean contains(boolean[] array, boolean value) { + return indexOf(array, value) > INDEX_NOT_FOUND; + } + + // ------------------------------------------------------------------- Wrap and unwrap + + /** + * 将原始类型数组包装为包装类型 + * + * @param values 原始类型数组 + * @return 包装类型数组 + */ + public static Integer[] wrap(int... values) { + if (null == values) { + return null; + } + final int length = values.length; + if (0 == length) { + return new Integer[0]; + } + + final Integer[] array = new Integer[length]; + for (int i = 0; i < length; i++) { + array[i] = values[i]; + } + return array; + } + + /** + * 包装类数组转为原始类型数组,null转为0 + * + * @param values 包装类型数组 + * @return 原始类型数组 + */ + public static int[] unWrap(Integer... values) { + if (null == values) { + return null; + } + final int length = values.length; + if (0 == length) { + return new int[0]; + } + + final int[] array = new int[length]; + for (int i = 0; i < length; i++) { + array[i] = ObjectUtil.defaultIfNull(values[i], 0); + } + return array; + } + + /** + * 将原始类型数组包装为包装类型 + * + * @param values 原始类型数组 + * @return 包装类型数组 + */ + public static Long[] wrap(long... values) { + if (null == values) { + return null; + } + final int length = values.length; + if (0 == length) { + return new Long[0]; + } + + final Long[] array = new Long[length]; + for (int i = 0; i < length; i++) { + array[i] = values[i]; + } + return array; + } + + /** + * 包装类数组转为原始类型数组 + * + * @param values 包装类型数组 + * @return 原始类型数组 + */ + public static long[] unWrap(Long... values) { + if (null == values) { + return null; + } + final int length = values.length; + if (0 == length) { + return new long[0]; + } + + final long[] array = new long[length]; + for (int i = 0; i < length; i++) { + array[i] = ObjectUtil.defaultIfNull(values[i], 0L); + } + return array; + } + + /** + * 将原始类型数组包装为包装类型 + * + * @param values 原始类型数组 + * @return 包装类型数组 + */ + public static Character[] wrap(char... values) { + if (null == values) { + return null; + } + final int length = values.length; + if (0 == length) { + return new Character[0]; + } + + final Character[] array = new Character[length]; + for (int i = 0; i < length; i++) { + array[i] = values[i]; + } + return array; + } + + /** + * 包装类数组转为原始类型数组 + * + * @param values 包装类型数组 + * @return 原始类型数组 + */ + public static char[] unWrap(Character... values) { + if (null == values) { + return null; + } + final int length = values.length; + if (0 == length) { + return new char[0]; + } + + char[] array = new char[length]; + for (int i = 0; i < length; i++) { + array[i] = ObjectUtil.defaultIfNull(values[i], Character.MIN_VALUE); + } + return array; + } + + /** + * 将原始类型数组包装为包装类型 + * + * @param values 原始类型数组 + * @return 包装类型数组 + */ + public static Byte[] wrap(byte... values) { + if (null == values) { + return null; + } + final int length = values.length; + if (0 == length) { + return new Byte[0]; + } + + final Byte[] array = new Byte[length]; + for (int i = 0; i < length; i++) { + array[i] = values[i]; + } + return array; + } + + /** + * 包装类数组转为原始类型数组 + * + * @param values 包装类型数组 + * @return 原始类型数组 + */ + public static byte[] unWrap(Byte... values) { + if (null == values) { + return null; + } + final int length = values.length; + if (0 == length) { + return new byte[0]; + } + + final byte[] array = new byte[length]; + for (int i = 0; i < length; i++) { + array[i] = ObjectUtil.defaultIfNull(values[i], (byte) 0); + } + return array; + } + + /** + * 将原始类型数组包装为包装类型 + * + * @param values 原始类型数组 + * @return 包装类型数组 + */ + public static Short[] wrap(short... values) { + if (null == values) { + return null; + } + final int length = values.length; + if (0 == length) { + return new Short[0]; + } + + final Short[] array = new Short[length]; + for (int i = 0; i < length; i++) { + array[i] = values[i]; + } + return array; + } + + /** + * 包装类数组转为原始类型数组 + * + * @param values 包装类型数组 + * @return 原始类型数组 + */ + public static short[] unWrap(Short... values) { + if (null == values) { + return null; + } + final int length = values.length; + if (0 == length) { + return new short[0]; + } + + final short[] array = new short[length]; + for (int i = 0; i < length; i++) { + array[i] = ObjectUtil.defaultIfNull(values[i], (short) 0); + } + return array; + } + + /** + * 将原始类型数组包装为包装类型 + * + * @param values 原始类型数组 + * @return 包装类型数组 + */ + public static Float[] wrap(float... values) { + if (null == values) { + return null; + } + final int length = values.length; + if (0 == length) { + return new Float[0]; + } + + final Float[] array = new Float[length]; + for (int i = 0; i < length; i++) { + array[i] = values[i]; + } + return array; + } + + /** + * 包装类数组转为原始类型数组 + * + * @param values 包装类型数组 + * @return 原始类型数组 + */ + public static float[] unWrap(Float... values) { + if (null == values) { + return null; + } + final int length = values.length; + if (0 == length) { + return new float[0]; + } + + final float[] array = new float[length]; + for (int i = 0; i < length; i++) { + array[i] = ObjectUtil.defaultIfNull(values[i], 0F); + } + return array; + } + + /** + * 将原始类型数组包装为包装类型 + * + * @param values 原始类型数组 + * @return 包装类型数组 + */ + public static Double[] wrap(double... values) { + if (null == values) { + return null; + } + final int length = values.length; + if (0 == length) { + return new Double[0]; + } + + final Double[] array = new Double[length]; + for (int i = 0; i < length; i++) { + array[i] = values[i]; + } + return array; + } + + /** + * 包装类数组转为原始类型数组 + * + * @param values 包装类型数组 + * @return 原始类型数组 + */ + public static double[] unWrap(Double... values) { + if (null == values) { + return null; + } + final int length = values.length; + if (0 == length) { + return new double[0]; + } + + final double[] array = new double[length]; + for (int i = 0; i < length; i++) { + array[i] = ObjectUtil.defaultIfNull(values[i], 0D); + } + return array; + } + + /** + * 将原始类型数组包装为包装类型 + * + * @param values 原始类型数组 + * @return 包装类型数组 + */ + public static Boolean[] wrap(boolean... values) { + if (null == values) { + return null; + } + final int length = values.length; + if (0 == length) { + return new Boolean[0]; + } + + final Boolean[] array = new Boolean[length]; + for (int i = 0; i < length; i++) { + array[i] = values[i]; + } + return array; + } + + /** + * 包装类数组转为原始类型数组
+ * {@code null} 按照 {@code false} 对待 + * + * @param values 包装类型数组 + * @return 原始类型数组 + */ + public static boolean[] unWrap(Boolean... values) { + if (null == values) { + return null; + } + final int length = values.length; + if (0 == length) { + return new boolean[0]; + } + + final boolean[] array = new boolean[length]; + for (int i = 0; i < length; i++) { + array[i] = ObjectUtil.defaultIfNull(values[i], false); + } + return array; + } + + // ------------------------------------------------------------------- sub + + /** + * 获取子数组 + * + * @param array 数组 + * @param start 开始位置(包括) + * @param end 结束位置(不包括) + * @return 新的数组 + * @see Arrays#copyOfRange(Object[], int, int) + * @since 4.5.2 + */ + public static byte[] sub(byte[] array, int start, int end) { + int length = Array.getLength(array); + if (start < 0) { + start += length; + } + if (end < 0) { + end += length; + } + if (start == length) { + return new byte[0]; + } + if (start > end) { + int tmp = start; + start = end; + end = tmp; + } + if (end > length) { + if (start >= length) { + return new byte[0]; + } + end = length; + } + return Arrays.copyOfRange(array, start, end); + } + + /** + * 获取子数组 + * + * @param array 数组 + * @param start 开始位置(包括) + * @param end 结束位置(不包括) + * @return 新的数组 + * @see Arrays#copyOfRange(Object[], int, int) + * @since 4.5.2 + */ + public static int[] sub(int[] array, int start, int end) { + int length = Array.getLength(array); + if (start < 0) { + start += length; + } + if (end < 0) { + end += length; + } + if (start == length) { + return new int[0]; + } + if (start > end) { + int tmp = start; + start = end; + end = tmp; + } + if (end > length) { + if (start >= length) { + return new int[0]; + } + end = length; + } + return Arrays.copyOfRange(array, start, end); + } + + /** + * 获取子数组 + * + * @param array 数组 + * @param start 开始位置(包括) + * @param end 结束位置(不包括) + * @return 新的数组 + * @see Arrays#copyOfRange(Object[], int, int) + * @since 4.5.2 + */ + public static long[] sub(long[] array, int start, int end) { + int length = Array.getLength(array); + if (start < 0) { + start += length; + } + if (end < 0) { + end += length; + } + if (start == length) { + return new long[0]; + } + if (start > end) { + int tmp = start; + start = end; + end = tmp; + } + if (end > length) { + if (start >= length) { + return new long[0]; + } + end = length; + } + return Arrays.copyOfRange(array, start, end); + } + + /** + * 获取子数组 + * + * @param array 数组 + * @param start 开始位置(包括) + * @param end 结束位置(不包括) + * @return 新的数组 + * @see Arrays#copyOfRange(Object[], int, int) + * @since 4.5.2 + */ + public static short[] sub(short[] array, int start, int end) { + int length = Array.getLength(array); + if (start < 0) { + start += length; + } + if (end < 0) { + end += length; + } + if (start == length) { + return new short[0]; + } + if (start > end) { + int tmp = start; + start = end; + end = tmp; + } + if (end > length) { + if (start >= length) { + return new short[0]; + } + end = length; + } + return Arrays.copyOfRange(array, start, end); + } + + /** + * 获取子数组 + * + * @param array 数组 + * @param start 开始位置(包括) + * @param end 结束位置(不包括) + * @return 新的数组 + * @see Arrays#copyOfRange(Object[], int, int) + * @since 4.5.2 + */ + public static char[] sub(char[] array, int start, int end) { + int length = Array.getLength(array); + if (start < 0) { + start += length; + } + if (end < 0) { + end += length; + } + if (start == length) { + return new char[0]; + } + if (start > end) { + int tmp = start; + start = end; + end = tmp; + } + if (end > length) { + if (start >= length) { + return new char[0]; + } + end = length; + } + return Arrays.copyOfRange(array, start, end); + } + + /** + * 获取子数组 + * + * @param array 数组 + * @param start 开始位置(包括) + * @param end 结束位置(不包括) + * @return 新的数组 + * @see Arrays#copyOfRange(Object[], int, int) + * @since 4.5.2 + */ + public static double[] sub(double[] array, int start, int end) { + int length = Array.getLength(array); + if (start < 0) { + start += length; + } + if (end < 0) { + end += length; + } + if (start == length) { + return new double[0]; + } + if (start > end) { + int tmp = start; + start = end; + end = tmp; + } + if (end > length) { + if (start >= length) { + return new double[0]; + } + end = length; + } + return Arrays.copyOfRange(array, start, end); + } + + /** + * 获取子数组 + * + * @param array 数组 + * @param start 开始位置(包括) + * @param end 结束位置(不包括) + * @return 新的数组 + * @see Arrays#copyOfRange(Object[], int, int) + * @since 4.5.2 + */ + public static float[] sub(float[] array, int start, int end) { + int length = Array.getLength(array); + if (start < 0) { + start += length; + } + if (end < 0) { + end += length; + } + if (start == length) { + return new float[0]; + } + if (start > end) { + int tmp = start; + start = end; + end = tmp; + } + if (end > length) { + if (start >= length) { + return new float[0]; + } + end = length; + } + return Arrays.copyOfRange(array, start, end); + } + + /** + * 获取子数组 + * + * @param array 数组 + * @param start 开始位置(包括) + * @param end 结束位置(不包括) + * @return 新的数组 + * @see Arrays#copyOfRange(Object[], int, int) + * @since 4.5.2 + */ + public static boolean[] sub(boolean[] array, int start, int end) { + int length = Array.getLength(array); + if (start < 0) { + start += length; + } + if (end < 0) { + end += length; + } + if (start == length) { + return new boolean[0]; + } + if (start > end) { + int tmp = start; + start = end; + end = tmp; + } + if (end > length) { + if (start >= length) { + return new boolean[0]; + } + end = length; + } + return Arrays.copyOfRange(array, start, end); + } + + // ------------------------------------------------------------------- remove + + /** + * 移除数组中对应位置的元素
+ * copy from commons-lang + * + * @param array 数组对象,可以是对象数组,也可以原始类型数组 + * @param index 位置,如果位置小于0或者大于长度,返回原数组 + * @return 去掉指定元素后的新数组或原数组 + * @throws IllegalArgumentException 参数对象不为数组对象 + * @since 3.0.8 + */ + public static long[] remove(long[] array, int index) throws IllegalArgumentException { + return (long[]) remove((Object) array, index); + } + + /** + * 移除数组中对应位置的元素
+ * copy from commons-lang + * + * @param array 数组对象,可以是对象数组,也可以原始类型数组 + * @param index 位置,如果位置小于0或者大于长度,返回原数组 + * @return 去掉指定元素后的新数组或原数组 + * @throws IllegalArgumentException 参数对象不为数组对象 + * @since 3.0.8 + */ + public static int[] remove(int[] array, int index) throws IllegalArgumentException { + return (int[]) remove((Object) array, index); + } + + /** + * 移除数组中对应位置的元素
+ * copy from commons-lang + * + * @param array 数组对象,可以是对象数组,也可以原始类型数组 + * @param index 位置,如果位置小于0或者大于长度,返回原数组 + * @return 去掉指定元素后的新数组或原数组 + * @throws IllegalArgumentException 参数对象不为数组对象 + * @since 3.0.8 + */ + public static short[] remove(short[] array, int index) throws IllegalArgumentException { + return (short[]) remove((Object) array, index); + } + + /** + * 移除数组中对应位置的元素
+ * copy from commons-lang + * + * @param array 数组对象,可以是对象数组,也可以原始类型数组 + * @param index 位置,如果位置小于0或者大于长度,返回原数组 + * @return 去掉指定元素后的新数组或原数组 + * @throws IllegalArgumentException 参数对象不为数组对象 + * @since 3.0.8 + */ + public static char[] remove(char[] array, int index) throws IllegalArgumentException { + return (char[]) remove((Object) array, index); + } + + /** + * 移除数组中对应位置的元素
+ * copy from commons-lang + * + * @param array 数组对象,可以是对象数组,也可以原始类型数组 + * @param index 位置,如果位置小于0或者大于长度,返回原数组 + * @return 去掉指定元素后的新数组或原数组 + * @throws IllegalArgumentException 参数对象不为数组对象 + * @since 3.0.8 + */ + public static byte[] remove(byte[] array, int index) throws IllegalArgumentException { + return (byte[]) remove((Object) array, index); + } + + /** + * 移除数组中对应位置的元素
+ * copy from commons-lang + * + * @param array 数组对象,可以是对象数组,也可以原始类型数组 + * @param index 位置,如果位置小于0或者大于长度,返回原数组 + * @return 去掉指定元素后的新数组或原数组 + * @throws IllegalArgumentException 参数对象不为数组对象 + * @since 3.0.8 + */ + public static double[] remove(double[] array, int index) throws IllegalArgumentException { + return (double[]) remove((Object) array, index); + } + + /** + * 移除数组中对应位置的元素
+ * copy from commons-lang + * + * @param array 数组对象,可以是对象数组,也可以原始类型数组 + * @param index 位置,如果位置小于0或者大于长度,返回原数组 + * @return 去掉指定元素后的新数组或原数组 + * @throws IllegalArgumentException 参数对象不为数组对象 + * @since 3.0.8 + */ + public static float[] remove(float[] array, int index) throws IllegalArgumentException { + return (float[]) remove((Object) array, index); + } + + /** + * 移除数组中对应位置的元素
+ * copy from commons-lang + * + * @param array 数组对象,可以是对象数组,也可以原始类型数组 + * @param index 位置,如果位置小于0或者大于长度,返回原数组 + * @return 去掉指定元素后的新数组或原数组 + * @throws IllegalArgumentException 参数对象不为数组对象 + * @since 3.0.8 + */ + public static boolean[] remove(boolean[] array, int index) throws IllegalArgumentException { + return (boolean[]) remove((Object) array, index); + } + + /** + * 移除数组中对应位置的元素
+ * copy from commons-lang + * + * @param array 数组对象,可以是对象数组,也可以原始类型数组 + * @param index 位置,如果位置小于0或者大于长度,返回原数组 + * @return 去掉指定元素后的新数组或原数组 + * @throws IllegalArgumentException 参数对象不为数组对象 + * @since 3.0.8 + */ + @SuppressWarnings("SuspiciousSystemArraycopy") + public static Object remove(Object array, int index) throws IllegalArgumentException { + if (null == array) { + return null; + } + int length = Array.getLength(array); + if (index < 0 || index >= length) { + return array; + } + + final Object result = Array.newInstance(array.getClass().getComponentType(), length - 1); + System.arraycopy(array, 0, result, 0, index); + if (index < length - 1) { + // 后半部分 + System.arraycopy(array, index + 1, result, index, length - index - 1); + } + + return result; + } + + // ---------------------------------------------------------------------- removeEle + + /** + * 移除数组中指定的元素
+ * 只会移除匹配到的第一个元素 copy from commons-lang + * + * @param array 数组对象,可以是对象数组,也可以原始类型数组 + * @param element 要移除的元素 + * @return 去掉指定元素后的新数组或原数组 + * @throws IllegalArgumentException 参数对象不为数组对象 + * @since 3.0.8 + */ + public static long[] removeEle(long[] array, long element) throws IllegalArgumentException { + return remove(array, indexOf(array, element)); + } + + /** + * 移除数组中指定的元素
+ * 只会移除匹配到的第一个元素 copy from commons-lang + * + * @param array 数组对象,可以是对象数组,也可以原始类型数组 + * @param element 要移除的元素 + * @return 去掉指定元素后的新数组或原数组 + * @throws IllegalArgumentException 参数对象不为数组对象 + * @since 3.0.8 + */ + public static int[] removeEle(int[] array, int element) throws IllegalArgumentException { + return remove(array, indexOf(array, element)); + } + + /** + * 移除数组中指定的元素
+ * 只会移除匹配到的第一个元素 copy from commons-lang + * + * @param array 数组对象,可以是对象数组,也可以原始类型数组 + * @param element 要移除的元素 + * @return 去掉指定元素后的新数组或原数组 + * @throws IllegalArgumentException 参数对象不为数组对象 + * @since 3.0.8 + */ + public static short[] removeEle(short[] array, short element) throws IllegalArgumentException { + return remove(array, indexOf(array, element)); + } + + /** + * 移除数组中指定的元素
+ * 只会移除匹配到的第一个元素 copy from commons-lang + * + * @param array 数组对象,可以是对象数组,也可以原始类型数组 + * @param element 要移除的元素 + * @return 去掉指定元素后的新数组或原数组 + * @throws IllegalArgumentException 参数对象不为数组对象 + * @since 3.0.8 + */ + public static char[] removeEle(char[] array, char element) throws IllegalArgumentException { + return remove(array, indexOf(array, element)); + } + + /** + * 移除数组中指定的元素
+ * 只会移除匹配到的第一个元素 copy from commons-lang + * + * @param array 数组对象,可以是对象数组,也可以原始类型数组 + * @param element 要移除的元素 + * @return 去掉指定元素后的新数组或原数组 + * @throws IllegalArgumentException 参数对象不为数组对象 + * @since 3.0.8 + */ + public static byte[] removeEle(byte[] array, byte element) throws IllegalArgumentException { + return remove(array, indexOf(array, element)); + } + + /** + * 移除数组中指定的元素
+ * 只会移除匹配到的第一个元素 copy from commons-lang + * + * @param array 数组对象,可以是对象数组,也可以原始类型数组 + * @param element 要移除的元素 + * @return 去掉指定元素后的新数组或原数组 + * @throws IllegalArgumentException 参数对象不为数组对象 + * @since 3.0.8 + */ + public static double[] removeEle(double[] array, double element) throws IllegalArgumentException { + return remove(array, indexOf(array, element)); + } + + /** + * 移除数组中指定的元素
+ * 只会移除匹配到的第一个元素 copy from commons-lang + * + * @param array 数组对象,可以是对象数组,也可以原始类型数组 + * @param element 要移除的元素 + * @return 去掉指定元素后的新数组或原数组 + * @throws IllegalArgumentException 参数对象不为数组对象 + * @since 3.0.8 + */ + public static float[] removeEle(float[] array, float element) throws IllegalArgumentException { + return remove(array, indexOf(array, element)); + } + + /** + * 移除数组中指定的元素
+ * 只会移除匹配到的第一个元素 copy from commons-lang + * + * @param array 数组对象,可以是对象数组,也可以原始类型数组 + * @param element 要移除的元素 + * @return 去掉指定元素后的新数组或原数组 + * @throws IllegalArgumentException 参数对象不为数组对象 + * @since 3.0.8 + */ + public static boolean[] removeEle(boolean[] array, boolean element) throws IllegalArgumentException { + return remove(array, indexOf(array, element)); + } + + // ---------------------------------------------------------------------- reverse + + /** + * 反转数组,会变更原数组 + * + * @param array 数组,会变更 + * @param startIndexInclusive 起始位置(包含) + * @param endIndexExclusive 结束位置(不包含) + * @return 变更后的原数组 + * @since 3.0.9 + */ + public static long[] reverse(long[] array, final int startIndexInclusive, final int endIndexExclusive) { + if (isEmpty(array)) { + return array; + } + int i = Math.max(startIndexInclusive, 0); + int j = Math.min(array.length, endIndexExclusive) - 1; + while (j > i) { + swap(array, i, j); + j--; + i++; + } + return array; + } + + /** + * 反转数组,会变更原数组 + * + * @param array 数组,会变更 + * @return 变更后的原数组 + * @since 3.0.9 + */ + public static long[] reverse(long[] array) { + return reverse(array, 0, array.length); + } + + /** + * 反转数组,会变更原数组 + * + * @param array 数组,会变更 + * @param startIndexInclusive 起始位置(包含) + * @param endIndexExclusive 结束位置(不包含) + * @return 变更后的原数组 + * @since 3.0.9 + */ + public static int[] reverse(int[] array, final int startIndexInclusive, final int endIndexExclusive) { + if (isEmpty(array)) { + return array; + } + int i = Math.max(startIndexInclusive, 0); + int j = Math.min(array.length, endIndexExclusive) - 1; + while (j > i) { + swap(array, i, j); + j--; + i++; + } + return array; + } + + /** + * 反转数组,会变更原数组 + * + * @param array 数组,会变更 + * @return 变更后的原数组 + * @since 3.0.9 + */ + public static int[] reverse(int[] array) { + return reverse(array, 0, array.length); + } + + /** + * 反转数组,会变更原数组 + * + * @param array 数组,会变更 + * @param startIndexInclusive 起始位置(包含) + * @param endIndexExclusive 结束位置(不包含) + * @return 变更后的原数组 + * @since 3.0.9 + */ + public static short[] reverse(short[] array, final int startIndexInclusive, final int endIndexExclusive) { + if (isEmpty(array)) { + return array; + } + int i = Math.max(startIndexInclusive, 0); + int j = Math.min(array.length, endIndexExclusive) - 1; + while (j > i) { + swap(array, i, j); + j--; + i++; + } + return array; + } + + /** + * 反转数组,会变更原数组 + * + * @param array 数组,会变更 + * @return 变更后的原数组 + * @since 3.0.9 + */ + public static short[] reverse(short[] array) { + return reverse(array, 0, array.length); + } + + /** + * 反转数组,会变更原数组 + * + * @param array 数组,会变更 + * @param startIndexInclusive 起始位置(包含) + * @param endIndexExclusive 结束位置(不包含) + * @return 变更后的原数组 + * @since 3.0.9 + */ + public static char[] reverse(char[] array, final int startIndexInclusive, final int endIndexExclusive) { + if (isEmpty(array)) { + return array; + } + int i = Math.max(startIndexInclusive, 0); + int j = Math.min(array.length, endIndexExclusive) - 1; + while (j > i) { + swap(array, i, j); + j--; + i++; + } + return array; + } + + /** + * 反转数组,会变更原数组 + * + * @param array 数组,会变更 + * @return 变更后的原数组 + * @since 3.0.9 + */ + public static char[] reverse(char[] array) { + return reverse(array, 0, array.length); + } + + /** + * 反转数组,会变更原数组 + * + * @param array 数组,会变更 + * @param startIndexInclusive 起始位置(包含) + * @param endIndexExclusive 结束位置(不包含) + * @return 变更后的原数组 + * @since 3.0.9 + */ + public static byte[] reverse(byte[] array, final int startIndexInclusive, final int endIndexExclusive) { + if (isEmpty(array)) { + return array; + } + int i = Math.max(startIndexInclusive, 0); + int j = Math.min(array.length, endIndexExclusive) - 1; + while (j > i) { + swap(array, i, j); + j--; + i++; + } + return array; + } + + /** + * 反转数组,会变更原数组 + * + * @param array 数组,会变更 + * @return 变更后的原数组 + * @since 3.0.9 + */ + public static byte[] reverse(byte[] array) { + return reverse(array, 0, array.length); + } + + /** + * 反转数组,会变更原数组 + * + * @param array 数组,会变更 + * @param startIndexInclusive 起始位置(包含) + * @param endIndexExclusive 结束位置(不包含) + * @return 变更后的原数组 + * @since 3.0.9 + */ + public static double[] reverse(double[] array, final int startIndexInclusive, final int endIndexExclusive) { + if (isEmpty(array)) { + return array; + } + int i = Math.max(startIndexInclusive, 0); + int j = Math.min(array.length, endIndexExclusive) - 1; + while (j > i) { + swap(array, i, j); + j--; + i++; + } + return array; + } + + /** + * 反转数组,会变更原数组 + * + * @param array 数组,会变更 + * @return 变更后的原数组 + * @since 3.0.9 + */ + public static double[] reverse(double[] array) { + return reverse(array, 0, array.length); + } + + /** + * 反转数组,会变更原数组 + * + * @param array 数组,会变更 + * @param startIndexInclusive 起始位置(包含) + * @param endIndexExclusive 结束位置(不包含) + * @return 变更后的原数组 + * @since 3.0.9 + */ + public static float[] reverse(float[] array, final int startIndexInclusive, final int endIndexExclusive) { + if (isEmpty(array)) { + return array; + } + int i = Math.max(startIndexInclusive, 0); + int j = Math.min(array.length, endIndexExclusive) - 1; + while (j > i) { + swap(array, i, j); + j--; + i++; + } + return array; + } + + /** + * 反转数组,会变更原数组 + * + * @param array 数组,会变更 + * @return 变更后的原数组 + * @since 3.0.9 + */ + public static float[] reverse(float[] array) { + return reverse(array, 0, array.length); + } + + /** + * 反转数组,会变更原数组 + * + * @param array 数组,会变更 + * @param startIndexInclusive 起始位置(包含) + * @param endIndexExclusive 结束位置(不包含) + * @return 变更后的原数组 + * @since 3.0.9 + */ + public static boolean[] reverse(boolean[] array, final int startIndexInclusive, final int endIndexExclusive) { + if (isEmpty(array)) { + return array; + } + int i = Math.max(startIndexInclusive, 0); + int j = Math.min(array.length, endIndexExclusive) - 1; + while (j > i) { + swap(array, i, j); + j--; + i++; + } + return array; + } + + /** + * 反转数组,会变更原数组 + * + * @param array 数组,会变更 + * @return 变更后的原数组 + * @since 3.0.9 + */ + public static boolean[] reverse(boolean[] array) { + return reverse(array, 0, array.length); + } + + // ------------------------------------------------------------------------------------------------------------ min and max + + /** + * 取最小值 + * + * @param numberArray 数字数组 + * @return 最小值 + * @since 3.0.9 + */ + public static long min(long... numberArray) { + if (isEmpty(numberArray)) { + throw new IllegalArgumentException("Number array must not empty !"); + } + long min = numberArray[0]; + for (int i = 1; i < numberArray.length; i++) { + if (min > numberArray[i]) { + min = numberArray[i]; + } + } + return min; + } + + /** + * 取最小值 + * + * @param numberArray 数字数组 + * @return 最小值 + * @since 3.0.9 + */ + public static int min(int... numberArray) { + if (isEmpty(numberArray)) { + throw new IllegalArgumentException("Number array must not empty !"); + } + int min = numberArray[0]; + for (int i = 1; i < numberArray.length; i++) { + if (min > numberArray[i]) { + min = numberArray[i]; + } + } + return min; + } + + /** + * 取最小值 + * + * @param numberArray 数字数组 + * @return 最小值 + * @since 3.0.9 + */ + public static short min(short... numberArray) { + if (isEmpty(numberArray)) { + throw new IllegalArgumentException("Number array must not empty !"); + } + short min = numberArray[0]; + for (int i = 1; i < numberArray.length; i++) { + if (min > numberArray[i]) { + min = numberArray[i]; + } + } + return min; + } + + /** + * 取最小值 + * + * @param numberArray 数字数组 + * @return 最小值 + * @since 3.0.9 + */ + public static char min(char... numberArray) { + if (isEmpty(numberArray)) { + throw new IllegalArgumentException("Number array must not empty !"); + } + char min = numberArray[0]; + for (int i = 1; i < numberArray.length; i++) { + if (min > numberArray[i]) { + min = numberArray[i]; + } + } + return min; + } + + /** + * 取最小值 + * + * @param numberArray 数字数组 + * @return 最小值 + * @since 3.0.9 + */ + public static byte min(byte... numberArray) { + if (isEmpty(numberArray)) { + throw new IllegalArgumentException("Number array must not empty !"); + } + byte min = numberArray[0]; + for (int i = 1; i < numberArray.length; i++) { + if (min > numberArray[i]) { + min = numberArray[i]; + } + } + return min; + } + + /** + * 取最小值 + * + * @param numberArray 数字数组 + * @return 最小值 + * @since 3.0.9 + */ + public static double min(double... numberArray) { + if (isEmpty(numberArray)) { + throw new IllegalArgumentException("Number array must not empty !"); + } + double min = numberArray[0]; + for (int i = 1; i < numberArray.length; i++) { + if (min > numberArray[i]) { + min = numberArray[i]; + } + } + return min; + } + + /** + * 取最小值 + * + * @param numberArray 数字数组 + * @return 最小值 + * @since 3.0.9 + */ + public static float min(float... numberArray) { + if (isEmpty(numberArray)) { + throw new IllegalArgumentException("Number array must not empty !"); + } + float min = numberArray[0]; + for (int i = 1; i < numberArray.length; i++) { + if (min > numberArray[i]) { + min = numberArray[i]; + } + } + return min; + } + + /** + * 取最大值 + * + * @param numberArray 数字数组 + * @return 最大值 + * @since 3.0.9 + */ + public static long max(long... numberArray) { + if (isEmpty(numberArray)) { + throw new IllegalArgumentException("Number array must not empty !"); + } + long max = numberArray[0]; + for (int i = 1; i < numberArray.length; i++) { + if (max < numberArray[i]) { + max = numberArray[i]; + } + } + return max; + } + + /** + * 取最大值 + * + * @param numberArray 数字数组 + * @return 最大值 + * @since 3.0.9 + */ + public static int max(int... numberArray) { + if (isEmpty(numberArray)) { + throw new IllegalArgumentException("Number array must not empty !"); + } + int max = numberArray[0]; + for (int i = 1; i < numberArray.length; i++) { + if (max < numberArray[i]) { + max = numberArray[i]; + } + } + return max; + } + + /** + * 取最大值 + * + * @param numberArray 数字数组 + * @return 最大值 + * @since 3.0.9 + */ + public static short max(short... numberArray) { + if (isEmpty(numberArray)) { + throw new IllegalArgumentException("Number array must not empty !"); + } + short max = numberArray[0]; + for (int i = 1; i < numberArray.length; i++) { + if (max < numberArray[i]) { + max = numberArray[i]; + } + } + return max; + } + + /** + * 取最大值 + * + * @param numberArray 数字数组 + * @return 最大值 + * @since 3.0.9 + */ + public static char max(char... numberArray) { + if (isEmpty(numberArray)) { + throw new IllegalArgumentException("Number array must not empty !"); + } + char max = numberArray[0]; + for (int i = 1; i < numberArray.length; i++) { + if (max < numberArray[i]) { + max = numberArray[i]; + } + } + return max; + } + + /** + * 取最大值 + * + * @param numberArray 数字数组 + * @return 最大值 + * @since 3.0.9 + */ + public static byte max(byte... numberArray) { + if (isEmpty(numberArray)) { + throw new IllegalArgumentException("Number array must not empty !"); + } + byte max = numberArray[0]; + for (int i = 1; i < numberArray.length; i++) { + if (max < numberArray[i]) { + max = numberArray[i]; + } + } + return max; + } + + /** + * 取最大值 + * + * @param numberArray 数字数组 + * @return 最大值 + * @since 3.0.9 + */ + public static double max(double... numberArray) { + if (isEmpty(numberArray)) { + throw new IllegalArgumentException("Number array must not empty !"); + } + double max = numberArray[0]; + for (int i = 1; i < numberArray.length; i++) { + if (max < numberArray[i]) { + max = numberArray[i]; + } + } + return max; + } + + /** + * 取最大值 + * + * @param numberArray 数字数组 + * @return 最大值 + * @since 3.0.9 + */ + public static float max(float... numberArray) { + if (isEmpty(numberArray)) { + throw new IllegalArgumentException("Number array must not empty !"); + } + float max = numberArray[0]; + for (int i = 1; i < numberArray.length; i++) { + if (max < numberArray[i]) { + max = numberArray[i]; + } + } + return max; + } + + // ---------------------------------------------------------------------- shuffle + + /** + * 打乱数组顺序,会变更原数组 + * + * @param array 数组,会变更 + * @return 打乱后的数组 + * @author FengBaoheng + * @since 5.5.2 + */ + public static int[] shuffle(int[] array) { + return shuffle(array, RandomUtil.getRandom()); + } + + /** + * 打乱数组顺序,会变更原数组 + * + * @param array 数组,会变更 + * @param random 随机数生成器 + * @return 打乱后的数组 + * @author FengBaoheng + * @since 5.5.2 + */ + public static int[] shuffle(int[] array, Random random) { + if (array == null || random == null || array.length <= 1) { + return array; + } + + for (int i = array.length; i > 1; i--) { + swap(array, i - 1, random.nextInt(i)); + } + + return array; + } + + /** + * 打乱数组顺序,会变更原数组 + * + * @param array 数组,会变更 + * @return 打乱后的数组 + * @author FengBaoheng + * @since 5.5.2 + */ + public static long[] shuffle(long[] array) { + return shuffle(array, RandomUtil.getRandom()); + } + + /** + * 打乱数组顺序,会变更原数组 + * + * @param array 数组,会变更 + * @param random 随机数生成器 + * @return 打乱后的数组 + * @author FengBaoheng + * @since 5.5.2 + */ + public static long[] shuffle(long[] array, Random random) { + if (array == null || random == null || array.length <= 1) { + return array; + } + + for (int i = array.length; i > 1; i--) { + swap(array, i - 1, random.nextInt(i)); + } + + return array; + } + + /** + * 打乱数组顺序,会变更原数组 + * + * @param array 数组,会变更 + * @return 打乱后的数组 + * @author FengBaoheng + * @since 5.5.2 + */ + public static double[] shuffle(double[] array) { + return shuffle(array, RandomUtil.getRandom()); + } + + /** + * 打乱数组顺序,会变更原数组 + * + * @param array 数组,会变更 + * @param random 随机数生成器 + * @return 打乱后的数组 + * @author FengBaoheng + * @since 5.5.2 + */ + public static double[] shuffle(double[] array, Random random) { + if (array == null || random == null || array.length <= 1) { + return array; + } + + for (int i = array.length; i > 1; i--) { + swap(array, i - 1, random.nextInt(i)); + } + + return array; + } + + /** + * 打乱数组顺序,会变更原数组 + * + * @param array 数组,会变更 + * @return 打乱后的数组 + * @author FengBaoheng + * @since 5.5.2 + */ + public static float[] shuffle(float[] array) { + return shuffle(array, RandomUtil.getRandom()); + } + + /** + * 打乱数组顺序,会变更原数组 + * + * @param array 数组,会变更 + * @param random 随机数生成器 + * @return 打乱后的数组 + * @author FengBaoheng + * @since 5.5.2 + */ + public static float[] shuffle(float[] array, Random random) { + if (array == null || random == null || array.length <= 1) { + return array; + } + + for (int i = array.length; i > 1; i--) { + swap(array, i - 1, random.nextInt(i)); + } + + return array; + } + + /** + * 打乱数组顺序,会变更原数组 + * + * @param array 数组,会变更 + * @return 打乱后的数组 + * @author FengBaoheng + * @since 5.5.2 + */ + public static boolean[] shuffle(boolean[] array) { + return shuffle(array, RandomUtil.getRandom()); + } + + /** + * 打乱数组顺序,会变更原数组 + * + * @param array 数组,会变更 + * @param random 随机数生成器 + * @return 打乱后的数组 + * @author FengBaoheng + * @since 5.5.2 + */ + public static boolean[] shuffle(boolean[] array, Random random) { + if (array == null || random == null || array.length <= 1) { + return array; + } + + for (int i = array.length; i > 1; i--) { + swap(array, i - 1, random.nextInt(i)); + } + + return array; + } + + /** + * 打乱数组顺序,会变更原数组 + * + * @param array 数组,会变更 + * @return 打乱后的数组 + * @author FengBaoheng + * @since 5.5.2 + */ + public static byte[] shuffle(byte[] array) { + return shuffle(array, RandomUtil.getRandom()); + } + + /** + * 打乱数组顺序,会变更原数组 + * + * @param array 数组,会变更 + * @param random 随机数生成器 + * @return 打乱后的数组 + * @author FengBaoheng + * @since 5.5.2 + */ + public static byte[] shuffle(byte[] array, Random random) { + if (array == null || random == null || array.length <= 1) { + return array; + } + + for (int i = array.length; i > 1; i--) { + swap(array, i - 1, random.nextInt(i)); + } + + return array; + } + + /** + * 打乱数组顺序,会变更原数组 + * + * @param array 数组,会变更 + * @return 打乱后的数组 + * @author FengBaoheng + * @since 5.5.2 + */ + public static char[] shuffle(char[] array) { + return shuffle(array, RandomUtil.getRandom()); + } + + /** + * 打乱数组顺序,会变更原数组 + * + * @param array 数组,会变更 + * @param random 随机数生成器 + * @return 打乱后的数组 + * @author FengBaoheng + * @since 5.5.2 + */ + public static char[] shuffle(char[] array, Random random) { + if (array == null || random == null || array.length <= 1) { + return array; + } + + for (int i = array.length; i > 1; i--) { + swap(array, i - 1, random.nextInt(i)); + } + + return array; + } + + /** + * 打乱数组顺序,会变更原数组 + * + * @param array 数组,会变更 + * @return 打乱后的数组 + * @author FengBaoheng + * @since 5.5.2 + */ + public static short[] shuffle(short[] array) { + return shuffle(array, RandomUtil.getRandom()); + } + + /** + * 打乱数组顺序,会变更原数组 + * + * @param array 数组,会变更 + * @param random 随机数生成器 + * @return 打乱后的数组 + * @author FengBaoheng + * @since 5.5.2 + */ + public static short[] shuffle(short[] array, Random random) { + if (array == null || random == null || array.length <= 1) { + return array; + } + + for (int i = array.length; i > 1; i--) { + swap(array, i - 1, random.nextInt(i)); + } + + return array; + } + + // ---------------------------------------------------------------------- swap + + /** + * 交换数组中两个位置的值 + * + * @param array 数组 + * @param index1 位置1 + * @param index2 位置2 + * @return 交换后的数组,与传入数组为同一对象 + * @since 4.0.7 + */ + public static int[] swap(int[] array, int index1, int index2) { + if (isEmpty(array)) { + throw new IllegalArgumentException("Number array must not empty !"); + } + int tmp = array[index1]; + array[index1] = array[index2]; + array[index2] = tmp; + return array; + } + + /** + * 交换数组中两个位置的值 + * + * @param array 数组 + * @param index1 位置1 + * @param index2 位置2 + * @return 交换后的数组,与传入数组为同一对象 + * @since 4.0.7 + */ + public static long[] swap(long[] array, int index1, int index2) { + if (isEmpty(array)) { + throw new IllegalArgumentException("Number array must not empty !"); + } + long tmp = array[index1]; + array[index1] = array[index2]; + array[index2] = tmp; + return array; + } + + /** + * 交换数组中两个位置的值 + * + * @param array 数组 + * @param index1 位置1 + * @param index2 位置2 + * @return 交换后的数组,与传入数组为同一对象 + * @since 4.0.7 + */ + public static double[] swap(double[] array, int index1, int index2) { + if (isEmpty(array)) { + throw new IllegalArgumentException("Number array must not empty !"); + } + double tmp = array[index1]; + array[index1] = array[index2]; + array[index2] = tmp; + return array; + } + + /** + * 交换数组中两个位置的值 + * + * @param array 数组 + * @param index1 位置1 + * @param index2 位置2 + * @return 交换后的数组,与传入数组为同一对象 + * @since 4.0.7 + */ + public static float[] swap(float[] array, int index1, int index2) { + if (isEmpty(array)) { + throw new IllegalArgumentException("Number array must not empty !"); + } + float tmp = array[index1]; + array[index1] = array[index2]; + array[index2] = tmp; + return array; + } + + /** + * 交换数组中两个位置的值 + * + * @param array 数组 + * @param index1 位置1 + * @param index2 位置2 + * @return 交换后的数组,与传入数组为同一对象 + * @since 4.0.7 + */ + public static boolean[] swap(boolean[] array, int index1, int index2) { + if (isEmpty(array)) { + throw new IllegalArgumentException("Number array must not empty !"); + } + boolean tmp = array[index1]; + array[index1] = array[index2]; + array[index2] = tmp; + return array; + } + + /** + * 交换数组中两个位置的值 + * + * @param array 数组 + * @param index1 位置1 + * @param index2 位置2 + * @return 交换后的数组,与传入数组为同一对象 + * @since 4.0.7 + */ + public static byte[] swap(byte[] array, int index1, int index2) { + if (isEmpty(array)) { + throw new IllegalArgumentException("Number array must not empty !"); + } + byte tmp = array[index1]; + array[index1] = array[index2]; + array[index2] = tmp; + return array; + } + + /** + * 交换数组中两个位置的值 + * + * @param array 数组 + * @param index1 位置1 + * @param index2 位置2 + * @return 交换后的数组,与传入数组为同一对象 + * @since 4.0.7 + */ + public static char[] swap(char[] array, int index1, int index2) { + if (isEmpty(array)) { + throw new IllegalArgumentException("Number array must not empty !"); + } + char tmp = array[index1]; + array[index1] = array[index2]; + array[index2] = tmp; + return array; + } + + /** + * 交换数组中两个位置的值 + * + * @param array 数组 + * @param index1 位置1 + * @param index2 位置2 + * @return 交换后的数组,与传入数组为同一对象 + * @since 4.0.7 + */ + public static short[] swap(short[] array, int index1, int index2) { + if (isEmpty(array)) { + throw new IllegalArgumentException("Number array must not empty !"); + } + short tmp = array[index1]; + array[index1] = array[index2]; + array[index2] = tmp; + return array; + } + + // ---------------------------------------------------------------------- asc and desc + + /** + * 检查数组是否升序,即array[i] <= array[i+1],若传入空数组,则返回false + * + * @param array 数组 + * @return 数组是否升序 + * @author FengBaoheng + * @since 5.5.2 + */ + public static boolean isSorted(byte[] array) { + return isSortedASC(array); + } + + /** + * 检查数组是否升序,即array[i] <= array[i+1],若传入空数组,则返回false + * + * @param array 数组 + * @return 数组是否升序 + * @author FengBaoheng + * @since 5.5.2 + */ + public static boolean isSortedASC(byte[] array) { + if (array == null) { + return false; + } + + for (int i = 0; i < array.length - 1; i++) { + if (array[i] > array[i + 1]) { + return false; + } + } + + return true; + } + + /** + * 检查数组是否降序,即array[i] >= array[i+1],若传入空数组,则返回false + * + * @param array 数组 + * @return 数组是否降序 + * @author FengBaoheng + * @since 5.5.2 + */ + public static boolean isSortedDESC(byte[] array) { + if (array == null) { + return false; + } + + for (int i = 0; i < array.length - 1; i++) { + if (array[i] < array[i + 1]) { + return false; + } + } + + return true; + } + + /** + * 检查数组是否升序,即array[i] <= array[i+1],若传入空数组,则返回false + * + * @param array 数组 + * @return 数组是否升序 + * @author FengBaoheng + * @since 5.5.2 + */ + public static boolean isSorted(short[] array) { + return isSortedASC(array); + } + + /** + * 检查数组是否升序,即array[i] <= array[i+1],若传入空数组,则返回false + * + * @param array 数组 + * @return 数组是否升序 + * @author FengBaoheng + * @since 5.5.2 + */ + public static boolean isSortedASC(short[] array) { + if (array == null) { + return false; + } + + for (int i = 0; i < array.length - 1; i++) { + if (array[i] > array[i + 1]) { + return false; + } + } + + return true; + } + + /** + * 检查数组是否降序,即array[i] >= array[i+1],若传入空数组,则返回false + * + * @param array 数组 + * @return 数组是否降序 + * @author FengBaoheng + * @since 5.5.2 + */ + public static boolean isSortedDESC(short[] array) { + if (array == null) { + return false; + } + + for (int i = 0; i < array.length - 1; i++) { + if (array[i] < array[i + 1]) { + return false; + } + } + + return true; + } + + /** + * 检查数组是否升序,即array[i] <= array[i+1],若传入空数组,则返回false + * + * @param array 数组 + * @return 数组是否升序 + * @author FengBaoheng + * @since 5.5.2 + */ + public static boolean isSorted(char[] array) { + return isSortedASC(array); + } + + /** + * 检查数组是否升序,即array[i] <= array[i+1],若传入空数组,则返回false + * + * @param array 数组 + * @return 数组是否升序 + * @author FengBaoheng + * @since 5.5.2 + */ + public static boolean isSortedASC(char[] array) { + if (array == null) { + return false; + } + + for (int i = 0; i < array.length - 1; i++) { + if (array[i] > array[i + 1]) { + return false; + } + } + + return true; + } + + /** + * 检查数组是否降序,即array[i] >= array[i+1],若传入空数组,则返回false + * + * @param array 数组 + * @return 数组是否降序 + * @author FengBaoheng + * @since 5.5.2 + */ + public static boolean isSortedDESC(char[] array) { + if (array == null) { + return false; + } + + for (int i = 0; i < array.length - 1; i++) { + if (array[i] < array[i + 1]) { + return false; + } + } + + return true; + } + + /** + * 检查数组是否升序,即array[i] <= array[i+1],若传入空数组,则返回false + * + * @param array 数组 + * @return 数组是否升序 + * @author FengBaoheng + * @since 5.5.2 + */ + public static boolean isSorted(int[] array) { + return isSortedASC(array); + } + + /** + * 检查数组是否升序,即array[i] <= array[i+1],若传入空数组,则返回false + * + * @param array 数组 + * @return 数组是否升序 + * @author FengBaoheng + * @since 5.5.2 + */ + public static boolean isSortedASC(int[] array) { + if (array == null) { + return false; + } + + for (int i = 0; i < array.length - 1; i++) { + if (array[i] > array[i + 1]) { + return false; + } + } + + return true; + } + + /** + * 检查数组是否降序,即array[i] >= array[i+1],若传入空数组,则返回false + * + * @param array 数组 + * @return 数组是否降序 + * @author FengBaoheng + * @since 5.5.2 + */ + public static boolean isSortedDESC(int[] array) { + if (array == null) { + return false; + } + + for (int i = 0; i < array.length - 1; i++) { + if (array[i] < array[i + 1]) { + return false; + } + } + + return true; + } + + /** + * 检查数组是否升序,即array[i] <= array[i+1],若传入空数组,则返回false + * + * @param array 数组 + * @return 数组是否升序 + * @author FengBaoheng + * @since 5.5.2 + */ + public static boolean isSorted(long[] array) { + return isSortedASC(array); + } + + /** + * 检查数组是否升序,即array[i] <= array[i+1],若传入空数组,则返回false + * + * @param array 数组 + * @return 数组是否升序 + * @author FengBaoheng + * @since 5.5.2 + */ + public static boolean isSortedASC(long[] array) { + if (array == null) { + return false; + } + + for (int i = 0; i < array.length - 1; i++) { + if (array[i] > array[i + 1]) { + return false; + } + } + + return true; + } + + /** + * 检查数组是否降序,即array[i] >= array[i+1],若传入空数组,则返回false + * + * @param array 数组 + * @return 数组是否降序 + * @author FengBaoheng + * @since 5.5.2 + */ + public static boolean isSortedDESC(long[] array) { + if (array == null) { + return false; + } + + for (int i = 0; i < array.length - 1; i++) { + if (array[i] < array[i + 1]) { + return false; + } + } + + return true; + } + + /** + * 检查数组是否升序,即array[i] <= array[i+1],若传入空数组,则返回false + * + * @param array 数组 + * @return 数组是否升序 + * @author FengBaoheng + * @since 5.5.2 + */ + public static boolean isSorted(double[] array) { + return isSortedASC(array); + } + + /** + * 检查数组是否升序,即array[i] <= array[i+1],若传入空数组,则返回false + * + * @param array 数组 + * @return 数组是否升序 + * @author FengBaoheng + * @since 5.5.2 + */ + public static boolean isSortedASC(double[] array) { + if (array == null) { + return false; + } + + for (int i = 0; i < array.length - 1; i++) { + if (array[i] > array[i + 1]) { + return false; + } + } + + return true; + } + + /** + * 检查数组是否降序,即array[i] >= array[i+1],若传入空数组,则返回false + * + * @param array 数组 + * @return 数组是否降序 + * @author FengBaoheng + * @since 5.5.2 + */ + public static boolean isSortedDESC(double[] array) { + if (array == null) { + return false; + } + + for (int i = 0; i < array.length - 1; i++) { + if (array[i] < array[i + 1]) { + return false; + } + } + + return true; + } + + /** + * 检查数组是否升序,即array[i] <= array[i+1],若传入空数组,则返回false + * + * @param array 数组 + * @return 数组是否升序 + * @author FengBaoheng + * @since 5.5.2 + */ + public static boolean isSorted(float[] array) { + return isSortedASC(array); + } + + /** + * 检查数组是否升序,即array[i] <= array[i+1],若传入空数组,则返回false + * + * @param array 数组 + * @return 数组是否升序 + * @author FengBaoheng + * @since 5.5.2 + */ + public static boolean isSortedASC(float[] array) { + if (array == null) { + return false; + } + + for (int i = 0; i < array.length - 1; i++) { + if (array[i] > array[i + 1]) { + return false; + } + } + + return true; + } + + /** + * 检查数组是否降序,即array[i] >= array[i+1],若传入空数组,则返回false + * + * @param array 数组 + * @return 数组是否降序 + * @author FengBaoheng + * @since 5.5.2 + */ + public static boolean isSortedDESC(float[] array) { + if (array == null) { + return false; + } + + for (int i = 0; i < array.length - 1; i++) { + if (array[i] < array[i + 1]) { + return false; + } + } + + return true; + } +} diff --git a/src/main/java/cn/hutool/core/util/RadixUtil.java b/src/main/java/cn/hutool/core/util/RadixUtil.java new file mode 100644 index 0000000..3185058 --- /dev/null +++ b/src/main/java/cn/hutool/core/util/RadixUtil.java @@ -0,0 +1,126 @@ +package cn.hutool.core.util; + +/** + * 进制转换工具类,可以转换为任意进制 + *

+ * 把一个十进制整数根据自己定义的进制规则进行转换
+ * from:https://gitee.com/loolly/hutool/pulls/260 + *

+ * 主要应用一下情况: + *

    + *
  • 根据ID生成邀请码,并且尽可能的缩短。并且不希望直接猜测出和ID的关联
  • + *
  • 短连接的生成,根据ID转成短连接,同样不希望被猜测到
  • + *
  • 数字加密,通过两次不同进制的转换,让有规律的数字看起来没有任何规律
  • + *
  • ....
  • + *
+ * + * @author xl7@qq.com + * @since 5.5.8 + */ + +public class RadixUtil { + /** + * 34进制字符串,不包含 IO 字符 + * 对于需要补齐的,自己可以随机填充IO字符 + * 26个字母:abcdefghijklmnopqrstuvwxyz + */ + public final static String RADIXS_34 = "0123456789ABCDEFGHJKLMNPQRSTUVWXYZ"; + /** + * 打乱后的34进制 + */ + public final static String RADIXS_SHUFFLE_34 = "H3UM16TDFPSBZJ90CW28QYRE45AXKNGV7L"; + + /** + * 59进制字符串,不包含 IOl 字符 + */ + public final static String RADIXS_59 = "0123456789abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ"; + /** + * 打乱后的59进制 + */ + public final static String RADIXS_SHUFFLE_59 = "vh9wGkfK8YmqbsoENP3764SeCX0dVzrgy1HRtpnTaLjJW2xQiZAcBMUFDu5"; + + /** + * 把一个整型数值转换成自己定义的进制 + * 长度即进制
+ *
    + *
  • encode("AB",10) 51转换成2进制,A=0;B=1 。 二进制1010,结果 BABA
  • + *
  • encode("VIP",21) 21转换成3进制,V=0;I=1;P=2 ,三进制210 ,得到结果PIV
  • + *
+ * + * @param radixs 自定进制,不要重复,否则转不回来的。 + * @param num 要转换的数值 + * @return 自定义进制字符串 + */ + public static String encode(String radixs, int num) { + //考虑到负数问题 + long tmpNum = (num >= 0 ? num : (0x100000000L - (~num + 1))); + return encode(radixs, tmpNum, 32); + } + + /** + * 把一个长整型数值转换成自己定义的进制 + * + * @param radixs 自定进制,不要重复,否则转不回来的。 + * @param num 要转换的数值 + * @return 自定义进制字符串 + */ + public static String encode(String radixs, long num) { + if (num < 0) { + throw new RuntimeException("暂不支持负数!"); + } + + return encode(radixs, num, 64); + } + + /** + * 把转换后的进制字符还原成int 值 + * + * @param radixs 自定进制,需要和encode的保持一致 + * @param encodeStr 需要转换成十进制的字符串 + * @return int + */ + public static int decodeToInt(String radixs, String encodeStr) { + //还原负数 + return (int) decode(radixs, encodeStr); + } + + /** + * 把转换后进制的字符还原成long 值 + * + * @param radixs 自定进制,需要和encode的保持一致 + * @param encodeStr 需要转换成十进制的字符串 + * @return long + */ + public static long decode(String radixs, String encodeStr) { + //目标是多少进制 + int rl = radixs.length(); + long res = 0L; + + for (char c : encodeStr.toCharArray()) { + res = res * rl + radixs.indexOf(c); + } + return res; + } + + // -------------------------------------------------------------------------------- Private methods + private static String encode(String radixs, long num, int maxLength) { + if (radixs.length() < 2) { + throw new RuntimeException("自定义进制最少两个字符哦!"); + } + //目标是多少进制 + int rl = radixs.length(); + //考虑到负数问题 + long tmpNum = num; + //进制的结果,二进制最小进制转换结果是32个字符 + //StringBuilder 比较耗时 + char[] aa = new char[maxLength]; + //因为反需字符串比较耗时 + int i = aa.length; + do { + aa[--i] = radixs.charAt((int) (tmpNum % rl)); + tmpNum /= rl; + } while (tmpNum > 0); + //去掉前面的字符串,trim比较耗时 + return new String(aa, i, aa.length - i); + } +} diff --git a/src/main/java/cn/hutool/core/util/RandomUtil.java b/src/main/java/cn/hutool/core/util/RandomUtil.java new file mode 100644 index 0000000..cf8d8ef --- /dev/null +++ b/src/main/java/cn/hutool/core/util/RandomUtil.java @@ -0,0 +1,651 @@ +package cn.hutool.core.util; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.collection.ListUtil; +import cn.hutool.core.date.DateField; +import cn.hutool.core.date.DateTime; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.exceptions.UtilException; +import cn.hutool.core.lang.WeightRandom; +import cn.hutool.core.lang.WeightRandom.WeightObj; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Date; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Random; +import java.util.Set; +import java.util.concurrent.ThreadLocalRandom; + +/** + * 随机工具类 + * + * @author xiaoleilu + */ +public class RandomUtil { + + /** + * 用于随机选的数字 + */ + public static final String BASE_NUMBER = "0123456789"; + /** + * 用于随机选的字符 + */ + public static final String BASE_CHAR = "abcdefghijklmnopqrstuvwxyz"; + /** + * 用于随机选的字符和数字 + */ + public static final String BASE_CHAR_NUMBER = BASE_CHAR + BASE_NUMBER; + + /** + * 获取随机数生成器对象
+ * ThreadLocalRandom是JDK 7之后提供并发产生随机数,能够解决多个线程发生的竞争争夺。 + * + *

+ * 注意:此方法返回的{@link ThreadLocalRandom}不可以在多线程环境下共享对象,否则有重复随机数问题。 + * 见:https://www.jianshu.com/p/89dfe990295c + *

+ * + * @return {@link ThreadLocalRandom} + * @since 3.1.2 + */ + public static ThreadLocalRandom getRandom() { + return ThreadLocalRandom.current(); + } + + /** + * 创建{@link SecureRandom},类提供加密的强随机数生成器 (RNG)
+ * + * @param seed 自定义随机种子 + * @return {@link SecureRandom} + * @since 4.6.5 + */ + public static SecureRandom createSecureRandom(byte[] seed) { + return (null == seed) ? new SecureRandom() : new SecureRandom(seed); + } + + /** + * 获取SHA1PRNG的{@link SecureRandom},类提供加密的强随机数生成器 (RNG)
+ * 注意:此方法获取的是伪随机序列发生器PRNG(pseudo-random number generator) + * + *

+ * 相关说明见:https://stackoverflow.com/questions/137212/how-to-solve-slow-java-securerandom + * + * @return {@link SecureRandom} + * @since 3.1.2 + */ + public static SecureRandom getSecureRandom() { + return getSecureRandom(null); + } + + /** + * 获取SHA1PRNG的{@link SecureRandom},类提供加密的强随机数生成器 (RNG)
+ * 注意:此方法获取的是伪随机序列发生器PRNG(pseudo-random number generator) + * + *

+ * 相关说明见:https://stackoverflow.com/questions/137212/how-to-solve-slow-java-securerandom + * + * @param seed 随机数种子 + * @return {@link SecureRandom} + * @see #createSecureRandom(byte[]) + * @since 5.5.2 + */ + public static SecureRandom getSecureRandom(byte[] seed) { + return createSecureRandom(seed); + } + + /** + * 获取SHA1PRNG的{@link SecureRandom},类提供加密的强随机数生成器 (RNG)
+ * 注意:此方法获取的是伪随机序列发生器PRNG(pseudo-random number generator),在Linux下噪声生成时可能造成较长时间停顿。
+ * see: http://ifeve.com/jvm-random-and-entropy-source/ + * + *

+ * 相关说明见:https://stackoverflow.com/questions/137212/how-to-solve-slow-java-securerandom + * + * @param seed 随机数种子 + * @return {@link SecureRandom} + * @since 5.5.8 + */ + public static SecureRandom getSHA1PRNGRandom(byte[] seed) { + SecureRandom random; + try { + random = SecureRandom.getInstance("SHA1PRNG"); + } catch (NoSuchAlgorithmException e) { + throw new UtilException(e); + } + if (null != seed) { + random.setSeed(seed); + } + return random; + } + + /** + * 获取algorithms/providers中提供的强安全随机生成器
+ * 注意:此方法可能造成阻塞或性能问题 + * + * @return {@link SecureRandom} + * @since 5.7.12 + */ + public static SecureRandom getSecureRandomStrong() { + try { + return SecureRandom.getInstanceStrong(); + } catch (NoSuchAlgorithmException e) { + throw new UtilException(e); + } + } + + /** + * 获取随机数产生器 + * + * @param isSecure 是否为强随机数生成器 (RNG) + * @return {@link Random} + * @see #getSecureRandom() + * @see #getRandom() + * @since 4.1.15 + */ + public static Random getRandom(boolean isSecure) { + return isSecure ? getSecureRandom() : getRandom(); + } + + /** + * 获得随机Boolean值 + * + * @return true or false + * @since 4.5.9 + */ + public static boolean randomBoolean() { + return 0 == randomInt(2); + } + + /** + * 随机汉字('\u4E00'-'\u9FFF') + * + * @return 随机的汉字字符 + * @since 5.7.15 + */ + public static char randomChinese() { + return (char) randomInt('\u4E00', '\u9FFF'); + } + + /** + * 获得指定范围内的随机数 + * + * @param min 最小数(包含) + * @param max 最大数(不包含) + * @return 随机数 + */ + public static int randomInt(int min, int max) { + return getRandom().nextInt(min, max); + } + + /** + * 获得随机数int值 + * + * @return 随机数 + * @see Random#nextInt() + */ + public static int randomInt() { + return getRandom().nextInt(); + } + + /** + * 获得指定范围内的随机数 [0,limit) + * + * @param limit 限制随机数的范围,不包括这个数 + * @return 随机数 + * @see Random#nextInt(int) + */ + public static int randomInt(int limit) { + return getRandom().nextInt(limit); + } + + /** + * 获得指定范围内的随机数[min, max) + * + * @param min 最小数(包含) + * @param max 最大数(不包含) + * @return 随机数 + * @see ThreadLocalRandom#nextLong(long, long) + * @since 3.3.0 + */ + public static long randomLong(long min, long max) { + return getRandom().nextLong(min, max); + } + + /** + * 获得随机数 + * + * @return 随机数 + * @see ThreadLocalRandom#nextLong() + * @since 3.3.0 + */ + public static long randomLong() { + return getRandom().nextLong(); + } + + /** + * 获得指定范围内的随机数 [0,limit) + * + * @param limit 限制随机数的范围,不包括这个数 + * @return 随机数 + * @see ThreadLocalRandom#nextLong(long) + */ + public static long randomLong(long limit) { + return getRandom().nextLong(limit); + } + + /** + * 获得指定范围内的随机数 + * + * @param min 最小数(包含) + * @param max 最大数(不包含) + * @return 随机数 + * @see ThreadLocalRandom#nextDouble(double, double) + * @since 3.3.0 + */ + public static double randomDouble(double min, double max) { + return getRandom().nextDouble(min, max); + } + + /** + * 获得指定范围内的随机数 + * + * @param min 最小数(包含) + * @param max 最大数(不包含) + * @param scale 保留小数位数 + * @param roundingMode 保留小数的模式 {@link RoundingMode} + * @return 随机数 + * @since 4.0.8 + */ + public static double randomDouble(double min, double max, int scale, RoundingMode roundingMode) { + return NumberUtil.round(randomDouble(min, max), scale, roundingMode).doubleValue(); + } + + /** + * 获得随机数[0, 1) + * + * @return 随机数 + * @see ThreadLocalRandom#nextDouble() + * @since 3.3.0 + */ + public static double randomDouble() { + return getRandom().nextDouble(); + } + + /** + * 获得指定范围内的随机数 + * + * @param scale 保留小数位数 + * @param roundingMode 保留小数的模式 {@link RoundingMode} + * @return 随机数 + * @since 4.0.8 + */ + public static double randomDouble(int scale, RoundingMode roundingMode) { + return NumberUtil.round(randomDouble(), scale, roundingMode).doubleValue(); + } + + /** + * 获得指定范围内的随机数 [0,limit) + * + * @param limit 限制随机数的范围,不包括这个数 + * @return 随机数 + * @see ThreadLocalRandom#nextDouble(double) + * @since 3.3.0 + */ + public static double randomDouble(double limit) { + return getRandom().nextDouble(limit); + } + + /** + * 获得指定范围内的随机数 + * + * @param limit 限制随机数的范围,不包括这个数 + * @param scale 保留小数位数 + * @param roundingMode 保留小数的模式 {@link RoundingMode} + * @return 随机数 + * @since 4.0.8 + */ + public static double randomDouble(double limit, int scale, RoundingMode roundingMode) { + return NumberUtil.round(randomDouble(limit), scale, roundingMode).doubleValue(); + } + + /** + * 获得指定范围内的随机数[0, 1) + * + * @return 随机数 + * @since 4.0.9 + */ + public static BigDecimal randomBigDecimal() { + return NumberUtil.toBigDecimal(getRandom().nextDouble()); + } + + /** + * 获得指定范围内的随机数 [0,limit) + * + * @param limit 最大数(不包含) + * @return 随机数 + * @since 4.0.9 + */ + public static BigDecimal randomBigDecimal(BigDecimal limit) { + return NumberUtil.toBigDecimal(getRandom().nextDouble(limit.doubleValue())); + } + + /** + * 获得指定范围内的随机数 + * + * @param min 最小数(包含) + * @param max 最大数(不包含) + * @return 随机数 + * @since 4.0.9 + */ + public static BigDecimal randomBigDecimal(BigDecimal min, BigDecimal max) { + return NumberUtil.toBigDecimal(getRandom().nextDouble(min.doubleValue(), max.doubleValue())); + } + + /** + * 随机bytes + * + * @param length 长度 + * @return bytes + */ + public static byte[] randomBytes(int length) { + byte[] bytes = new byte[length]; + getRandom().nextBytes(bytes); + return bytes; + } + + /** + * 随机获得列表中的元素 + * + * @param 元素类型 + * @param list 列表 + * @return 随机元素 + */ + public static T randomEle(List list) { + return randomEle(list, list.size()); + } + + /** + * 随机获得列表中的元素 + * + * @param 元素类型 + * @param list 列表 + * @param limit 限制列表的前N项 + * @return 随机元素 + */ + public static T randomEle(List list, int limit) { + if (list.size() < limit) { + limit = list.size(); + } + return list.get(randomInt(limit)); + } + + /** + * 随机获得数组中的元素 + * + * @param 元素类型 + * @param array 列表 + * @return 随机元素 + * @since 3.3.0 + */ + public static T randomEle(T[] array) { + return randomEle(array, array.length); + } + + /** + * 随机获得数组中的元素 + * + * @param 元素类型 + * @param array 列表 + * @param limit 限制列表的前N项 + * @return 随机元素 + * @since 3.3.0 + */ + public static T randomEle(T[] array, int limit) { + if (array.length < limit) { + limit = array.length; + } + return array[randomInt(limit)]; + } + + /** + * 随机获得列表中的一定量元素 + * + * @param 元素类型 + * @param list 列表 + * @param count 随机取出的个数 + * @return 随机元素 + */ + public static List randomEles(List list, int count) { + final List result = new ArrayList<>(count); + int limit = list.size(); + while (result.size() < count) { + result.add(randomEle(list, limit)); + } + + return result; + } + + /** + * 随机获得列表中的一定量的元素,返回List
+ * 此方法与{@link #randomEles(List, int)} 不同点在于,不会获取重复位置的元素 + * + * @param source 列表 + * @param count 随机取出的个数 + * @param 元素类型 + * @return 随机列表 + * @since 5.2.1 + */ + public static List randomEleList(List source, int count) { + if (count >= source.size()) { + return ListUtil.toList(source); + } + final int[] randomList = ArrayUtil.sub(randomInts(source.size()), 0, count); + List result = new ArrayList<>(); + for (int e : randomList) { + result.add(source.get(e)); + } + return result; + } + + /** + * 随机获得列表中的一定量的不重复元素,返回Set + * + * @param 元素类型 + * @param collection 列表 + * @param count 随机取出的个数 + * @return 随机元素 + * @throws IllegalArgumentException 需要的长度大于给定集合非重复总数 + */ + public static Set randomEleSet(Collection collection, int count) { + final ArrayList source = CollUtil.distinct(collection); + if (count > source.size()) { + throw new IllegalArgumentException("Count is larger than collection distinct size !"); + } + + final Set result = new LinkedHashSet<>(count); + int limit = source.size(); + while (result.size() < count) { + result.add(randomEle(source, limit)); + } + + return result; + } + + /** + * 创建指定长度的随机索引 + * + * @param length 长度 + * @return 随机索引 + * @since 5.2.1 + */ + public static int[] randomInts(int length) { + final int[] range = ArrayUtil.range(length); + for (int i = 0; i < length; i++) { + int random = randomInt(i, length); + ArrayUtil.swap(range, i, random); + } + return range; + } + + /** + * 获得一个随机的字符串(只包含数字和字符) + * + * @param length 字符串的长度 + * @return 随机字符串 + */ + public static String randomString(int length) { + return randomString(BASE_CHAR_NUMBER, length); + } + + /** + * 获得一个随机的字符串(只包含数字和大写字符) + * + * @param length 字符串的长度 + * @return 随机字符串 + * @since 4.0.13 + */ + public static String randomStringUpper(int length) { + return randomString(BASE_CHAR_NUMBER, length).toUpperCase(); + } + + /** + * 获得一个随机的字符串(只包含数字和小写字母) 并排除指定字符串 + * + * @param length 字符串的长度 + * @param elemData 要排除的字符串,如:去重容易混淆的字符串,oO0、lL1、q9Q、pP,不区分大小写 + * @return 随机字符串 + */ + public static String randomStringWithoutStr(int length, String elemData) { + String baseStr = BASE_CHAR_NUMBER; + baseStr = StrUtil.removeAll(baseStr, elemData.toLowerCase().toCharArray()); + return randomString(baseStr, length); + } + + /** + * 获得一个只包含数字的字符串 + * + * @param length 字符串的长度 + * @return 随机字符串 + */ + public static String randomNumbers(int length) { + return randomString(BASE_NUMBER, length); + } + + /** + * 获得一个随机的字符串 + * + * @param baseString 随机字符选取的样本 + * @param length 字符串的长度 + * @return 随机字符串 + */ + public static String randomString(String baseString, int length) { + if (StrUtil.isEmpty(baseString)) { + return StrUtil.EMPTY; + } + if (length < 1) { + length = 1; + } + + final StringBuilder sb = new StringBuilder(length); + int baseLength = baseString.length(); + for (int i = 0; i < length; i++) { + int number = randomInt(baseLength); + sb.append(baseString.charAt(number)); + } + return sb.toString(); + } + + /** + * 随机数字,数字为0~9单个数字 + * + * @return 随机数字字符 + * @since 3.1.2 + */ + public static char randomNumber() { + return randomChar(BASE_NUMBER); + } + + /** + * 随机字母或数字,小写 + * + * @return 随机字符 + * @since 3.1.2 + */ + public static char randomChar() { + return randomChar(BASE_CHAR_NUMBER); + } + + /** + * 随机字符 + * + * @param baseString 随机字符选取的样本 + * @return 随机字符 + * @since 3.1.2 + */ + public static char randomChar(String baseString) { + return baseString.charAt(randomInt(baseString.length())); + } + + + + /** + * 带有权重的随机生成器 + * + * @param 随机对象类型 + * @param weightObjs 带有权重的对象列表 + * @return {@link WeightRandom} + * @since 4.0.3 + */ + public static WeightRandom weightRandom(WeightObj[] weightObjs) { + return new WeightRandom<>(weightObjs); + } + + /** + * 带有权重的随机生成器 + * + * @param 随机对象类型 + * @param weightObjs 带有权重的对象列表 + * @return {@link WeightRandom} + * @since 4.0.3 + */ + public static WeightRandom weightRandom(Iterable> weightObjs) { + return new WeightRandom<>(weightObjs); + } + + /** + * 以当天为基准,随机产生一个日期 + * + * @param min 偏移最小天,可以为负数表示过去的时间(包含) + * @param max 偏移最大天,可以为负数表示过去的时间(不包含) + * @return 随机日期(随机天,其它时间不变) + * @since 4.0.8 + */ + public static DateTime randomDay(int min, int max) { + return randomDate(DateUtil.date(), DateField.DAY_OF_YEAR, min, max); + } + + /** + * 以给定日期为基准,随机产生一个日期 + * + * @param baseDate 基准日期 + * @param dateField 偏移的时间字段,例如时、分、秒等 + * @param min 偏移最小量,可以为负数表示过去的时间(包含) + * @param max 偏移最大量,可以为负数表示过去的时间(不包含) + * @return 随机日期 + * @since 4.5.8 + */ + public static DateTime randomDate(Date baseDate, DateField dateField, int min, int max) { + if (null == baseDate) { + baseDate = DateUtil.date(); + } + + return DateUtil.offset(baseDate, dateField, randomInt(min, max)); + } + +} diff --git a/src/main/java/cn/hutool/core/util/ReUtil.java b/src/main/java/cn/hutool/core/util/ReUtil.java new file mode 100644 index 0000000..64b85d7 --- /dev/null +++ b/src/main/java/cn/hutool/core/util/ReUtil.java @@ -0,0 +1,989 @@ +package cn.hutool.core.util; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.comparator.LengthComparator; +import cn.hutool.core.convert.Convert; +import cn.hutool.core.exceptions.UtilException; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.lang.PatternPool; +import cn.hutool.core.lang.RegexPool; +import cn.hutool.core.lang.Validator; +import cn.hutool.core.lang.func.Func1; +import cn.hutool.core.lang.mutable.Mutable; +import cn.hutool.core.lang.mutable.MutableObj; +import cn.hutool.core.map.MapUtil; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeSet; +import java.util.function.Consumer; +import java.util.regex.MatchResult; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * 正则相关工具类
+ * 常用正则请见 {@link Validator} + * + * @author xiaoleilu + */ +public class ReUtil { + + /** + * 正则表达式匹配中文汉字 + */ + public final static String RE_CHINESE = RegexPool.CHINESE; + /** + * 正则表达式匹配中文字符串 + */ + public final static String RE_CHINESES = RegexPool.CHINESES; + + /** + * 正则中需要被转义的关键字 + */ + public final static Set RE_KEYS = CollUtil.newHashSet('$', '(', ')', '*', '+', '.', '[', ']', '?', '\\', '^', '{', '}', '|'); + + /** + * 获得匹配的字符串,获得正则中分组0的内容 + * + * @param regex 匹配的正则 + * @param content 被匹配的内容 + * @return 匹配后得到的字符串,未匹配返回null + * @since 3.1.2 + */ + public static String getGroup0(String regex, CharSequence content) { + return get(regex, content, 0); + } + + /** + * 获得匹配的字符串,获得正则中分组1的内容 + * + * @param regex 匹配的正则 + * @param content 被匹配的内容 + * @return 匹配后得到的字符串,未匹配返回null + * @since 3.1.2 + */ + public static String getGroup1(String regex, CharSequence content) { + return get(regex, content, 1); + } + + /** + * 获得匹配的字符串 + * + * @param regex 匹配的正则 + * @param content 被匹配的内容 + * @param groupIndex 匹配正则的分组序号 + * @return 匹配后得到的字符串,未匹配返回null + */ + public static String get(String regex, CharSequence content, int groupIndex) { + if (null == content || null == regex) { + return null; + } + + final Pattern pattern = PatternPool.get(regex, Pattern.DOTALL); + return get(pattern, content, groupIndex); + } + + /** + * 获得匹配的字符串 + * + * @param regex 匹配的正则 + * @param content 被匹配的内容 + * @param groupName 匹配正则的分组名称 + * @return 匹配后得到的字符串,未匹配返回null + */ + public static String get(String regex, CharSequence content, String groupName) { + if (null == content || null == regex) { + return null; + } + + final Pattern pattern = PatternPool.get(regex, Pattern.DOTALL); + return get(pattern, content, groupName); + } + + /** + * 获得匹配的字符串,获得正则中分组0的内容 + * + * @param pattern 编译后的正则模式 + * @param content 被匹配的内容 + * @return 匹配后得到的字符串,未匹配返回null + * @since 3.1.2 + */ + public static String getGroup0(Pattern pattern, CharSequence content) { + return get(pattern, content, 0); + } + + /** + * 获得匹配的字符串,获得正则中分组1的内容 + * + * @param pattern 编译后的正则模式 + * @param content 被匹配的内容 + * @return 匹配后得到的字符串,未匹配返回null + * @since 3.1.2 + */ + public static String getGroup1(Pattern pattern, CharSequence content) { + return get(pattern, content, 1); + } + + /** + * 获得匹配的字符串,对应分组0表示整个匹配内容,1表示第一个括号分组内容,依次类推 + * + * @param pattern 编译后的正则模式 + * @param content 被匹配的内容 + * @param groupIndex 匹配正则的分组序号,0表示整个匹配内容,1表示第一个括号分组内容,依次类推 + * @return 匹配后得到的字符串,未匹配返回null + */ + public static String get(Pattern pattern, CharSequence content, int groupIndex) { + if (null == content || null == pattern) { + return null; + } + + final MutableObj result = new MutableObj<>(); + get(pattern, content, matcher -> result.set(matcher.group(groupIndex))); + return result.get(); + } + + /** + * 获得匹配的字符串 + * + * @param pattern 匹配的正则 + * @param content 被匹配的内容 + * @param groupName 匹配正则的分组名称 + * @return 匹配后得到的字符串,未匹配返回null + * @since 5.7.15 + */ + public static String get(Pattern pattern, CharSequence content, String groupName) { + if (null == content || null == pattern || null == groupName) { + return null; + } + + final MutableObj result = new MutableObj<>(); + get(pattern, content, matcher -> result.set(matcher.group(groupName))); + return result.get(); + } + + /** + * 在给定字符串中查找给定规则的字符,如果找到则使用{@link Consumer}处理之
+ * 如果内容中有多个匹配项,则只处理找到的第一个结果。 + * + * @param pattern 匹配的正则 + * @param content 被匹配的内容 + * @param consumer 匹配到的内容处理器 + * @since 5.7.15 + */ + public static void get(Pattern pattern, CharSequence content, Consumer consumer) { + if (null == content || null == pattern || null == consumer) { + return; + } + final Matcher m = pattern.matcher(content); + if (m.find()) { + consumer.accept(m); + } + } + + /** + * 获得匹配的字符串匹配到的所有分组 + * + * @param pattern 编译后的正则模式 + * @param content 被匹配的内容 + * @return 匹配后得到的字符串数组,按照分组顺序依次列出,未匹配到返回空列表,任何一个参数为null返回null + * @since 3.1.0 + */ + public static List getAllGroups(Pattern pattern, CharSequence content) { + return getAllGroups(pattern, content, true); + } + + /** + * 获得匹配的字符串匹配到的所有分组 + * + * @param pattern 编译后的正则模式 + * @param content 被匹配的内容 + * @param withGroup0 是否包括分组0,此分组表示全匹配的信息 + * @return 匹配后得到的字符串数组,按照分组顺序依次列出,未匹配到返回空列表,任何一个参数为null返回null + * @since 4.0.13 + */ + public static List getAllGroups(Pattern pattern, CharSequence content, boolean withGroup0) { + return getAllGroups(pattern, content, withGroup0, false); + } + + /** + * 获得匹配的字符串匹配到的所有分组 + * + * @param pattern 编译后的正则模式 + * @param content 被匹配的内容 + * @param withGroup0 是否包括分组0,此分组表示全匹配的信息 + * @param findAll 是否查找所有匹配到的内容,{@code false}表示只读取第一个匹配到的内容 + * @return 匹配后得到的字符串数组,按照分组顺序依次列出,未匹配到返回空列表,任何一个参数为null返回null + * @since 4.0.13 + */ + public static List getAllGroups(Pattern pattern, CharSequence content, boolean withGroup0, boolean findAll) { + if (null == content || null == pattern) { + return null; + } + + ArrayList result = new ArrayList<>(); + final Matcher matcher = pattern.matcher(content); + while (matcher.find()) { + final int startGroup = withGroup0 ? 0 : 1; + final int groupCount = matcher.groupCount(); + for (int i = startGroup; i <= groupCount; i++) { + result.add(matcher.group(i)); + } + + if(!findAll){ + break; + } + } + return result; + } + + /** + * 根据给定正则查找字符串中的匹配项,返回所有匹配的分组名对应分组值
+ *

+	 * pattern: (?<year>\\d+)-(?<month>\\d+)-(?<day>\\d+)
+	 * content: 2021-10-11
+	 * result : year: 2021, month: 10, day: 11
+	 * 
+ * + * @param pattern 匹配的正则 + * @param content 被匹配的内容 + * @return 命名捕获组,key为分组名,value为对应值 + * @since 5.7.15 + */ + public static Map getAllGroupNames(Pattern pattern, CharSequence content) { + if (null == content || null == pattern) { + return null; + } + final Matcher m = pattern.matcher(content); + final Map result = MapUtil.newHashMap(m.groupCount()); + if (m.find()) { + // 通过反射获取 namedGroups 方法 + final Map map = ReflectUtil.invoke(pattern, "namedGroups"); + map.forEach((key, value) -> result.put(key, m.group(value))); + } + return result; + } + + /** + * 从content中匹配出多个值并根据template生成新的字符串
+ * 例如:
+ * content 2013年5月 pattern (.*?)年(.*?)月 template: $1-$2 return 2013-5 + * + * @param pattern 匹配正则 + * @param content 被匹配的内容 + * @param template 生成内容模板,变量 $1 表示group1的内容,以此类推 + * @return 新字符串 + */ + public static String extractMulti(Pattern pattern, CharSequence content, String template) { + if (null == content || null == pattern || null == template) { + return null; + } + + //提取模板中的编号 + final TreeSet varNums = new TreeSet<>((o1, o2) -> ObjectUtil.compare(o2, o1)); + final Matcher matcherForTemplate = PatternPool.GROUP_VAR.matcher(template); + while (matcherForTemplate.find()) { + varNums.add(Integer.parseInt(matcherForTemplate.group(1))); + } + + final Matcher matcher = pattern.matcher(content); + if (matcher.find()) { + for (Integer group : varNums) { + template = template.replace("$" + group, matcher.group(group)); + } + return template; + } + return null; + } + + /** + * 从content中匹配出多个值并根据template生成新的字符串
+ * 匹配结束后会删除匹配内容之前的内容(包括匹配内容)
+ * 例如:
+ * content 2013年5月 pattern (.*?)年(.*?)月 template: $1-$2 return 2013-5 + * + * @param regex 匹配正则字符串 + * @param content 被匹配的内容 + * @param template 生成内容模板,变量 $1 表示group1的内容,以此类推 + * @return 按照template拼接后的字符串 + */ + public static String extractMulti(String regex, CharSequence content, String template) { + if (null == content || null == regex || null == template) { + return null; + } + + final Pattern pattern = PatternPool.get(regex, Pattern.DOTALL); + return extractMulti(pattern, content, template); + } + + /** + * 从content中匹配出多个值并根据template生成新的字符串
+ * 匹配结束后会删除匹配内容之前的内容(包括匹配内容)
+ * 例如:
+ * content 2013年5月 pattern (.*?)年(.*?)月 template: $1-$2 return 2013-5 + * + * @param pattern 匹配正则 + * @param contentHolder 被匹配的内容的Holder,value为内容正文,经过这个方法的原文将被去掉匹配之前的内容 + * @param template 生成内容模板,变量 $1 表示group1的内容,以此类推 + * @return 新字符串 + * @since 5.8.0 + */ + public static String extractMultiAndDelPre(Pattern pattern, Mutable contentHolder, String template) { + if (null == contentHolder || null == pattern || null == template) { + return null; + } + + HashSet varNums = findAll(PatternPool.GROUP_VAR, template, 1, new HashSet<>()); + + final CharSequence content = contentHolder.get(); + Matcher matcher = pattern.matcher(content); + if (matcher.find()) { + for (String var : varNums) { + int group = Integer.parseInt(var); + template = template.replace("$" + var, matcher.group(group)); + } + contentHolder.set(StrUtil.sub(content, matcher.end(), content.length())); + return template; + } + return null; + } + + /** + * 从content中匹配出多个值并根据template生成新的字符串
+ * 例如:
+ * content 2013年5月 pattern (.*?)年(.*?)月 template: $1-$2 return 2013-5 + * + * @param regex 匹配正则字符串 + * @param contentHolder 被匹配的内容的Holder,value为内容正文,经过这个方法的原文将被去掉匹配之前的内容 + * @param template 生成内容模板,变量 $1 表示group1的内容,以此类推 + * @return 按照template拼接后的字符串 + */ + public static String extractMultiAndDelPre(String regex, Mutable contentHolder, String template) { + if (null == contentHolder || null == regex || null == template) { + return null; + } + + final Pattern pattern = PatternPool.get(regex, Pattern.DOTALL); + return extractMultiAndDelPre(pattern, contentHolder, template); + } + + /** + * 删除匹配的第一个内容 + * + * @param regex 正则 + * @param content 被匹配的内容 + * @return 删除后剩余的内容 + */ + public static String delFirst(String regex, CharSequence content) { + if (StrUtil.hasBlank(regex, content)) { + return StrUtil.str(content); + } + + final Pattern pattern = PatternPool.get(regex, Pattern.DOTALL); + return delFirst(pattern, content); + } + + /** + * 删除匹配的第一个内容 + * + * @param pattern 正则 + * @param content 被匹配的内容 + * @return 删除后剩余的内容 + */ + public static String delFirst(Pattern pattern, CharSequence content) { + return replaceFirst(pattern, content, StrUtil.EMPTY); + } + + /** + * 替换匹配的第一个内容 + * + * @param pattern 正则 + * @param content 被匹配的内容 + * @param replacement 替换的内容 + * @return 替换后剩余的内容 + * @since 5.6.5 + */ + public static String replaceFirst(Pattern pattern, CharSequence content, String replacement) { + if (null == pattern || StrUtil.isEmpty(content)) { + return StrUtil.str(content); + } + + return pattern.matcher(content).replaceFirst(replacement); + } + + /** + * 删除匹配的最后一个内容 + * + * @param regex 正则 + * @param str 被匹配的内容 + * @return 删除后剩余的内容 + * @since 5.6.5 + */ + public static String delLast(String regex, CharSequence str) { + if (StrUtil.hasBlank(regex, str)) { + return StrUtil.str(str); + } + + final Pattern pattern = PatternPool.get(regex, Pattern.DOTALL); + return delLast(pattern, str); + } + + /** + * 删除匹配的最后一个内容 + * + * @param pattern 正则 + * @param str 被匹配的内容 + * @return 删除后剩余的内容 + * @since 5.6.5 + */ + public static String delLast(Pattern pattern, CharSequence str) { + if (null != pattern && StrUtil.isNotEmpty(str)) { + final MatchResult matchResult = lastIndexOf(pattern, str); + if (null != matchResult) { + return StrUtil.subPre(str, matchResult.start()) + StrUtil.subSuf(str, matchResult.end()); + } + } + + return StrUtil.str(str); + } + + /** + * 删除匹配的全部内容 + * + * @param regex 正则 + * @param content 被匹配的内容 + * @return 删除后剩余的内容 + */ + public static String delAll(String regex, CharSequence content) { + if (StrUtil.hasEmpty(regex, content)) { + return StrUtil.str(content); + } + + final Pattern pattern = PatternPool.get(regex, Pattern.DOTALL); + return delAll(pattern, content); + } + + /** + * 删除匹配的全部内容 + * + * @param pattern 正则 + * @param content 被匹配的内容 + * @return 删除后剩余的内容 + */ + public static String delAll(Pattern pattern, CharSequence content) { + if (null == pattern || StrUtil.isEmpty(content)) { + return StrUtil.str(content); + } + + return pattern.matcher(content).replaceAll(StrUtil.EMPTY); + } + + /** + * 删除正则匹配到的内容之前的字符 如果没有找到,则返回原文 + * + * @param regex 定位正则 + * @param content 被查找的内容 + * @return 删除前缀后的新内容 + */ + public static String delPre(String regex, CharSequence content) { + if (null == content || null == regex) { + return StrUtil.str(content); + } + + final Pattern pattern = PatternPool.get(regex, Pattern.DOTALL); + return delPre(pattern, content); + } + + /** + * 删除正则匹配到的内容之前的字符 如果没有找到,则返回原文 + * + * @param pattern 定位正则模式 + * @param content 被查找的内容 + * @return 删除前缀后的新内容 + */ + public static String delPre(Pattern pattern, CharSequence content) { + if (null == content || null == pattern) { + return StrUtil.str(content); + } + + final Matcher matcher = pattern.matcher(content); + if (matcher.find()) { + return StrUtil.sub(content, matcher.end(), content.length()); + } + return StrUtil.str(content); + } + + /** + * 取得内容中匹配的所有结果,获得匹配的所有结果中正则对应分组0的内容 + * + * @param regex 正则 + * @param content 被查找的内容 + * @return 结果列表 + * @since 3.1.2 + */ + public static List findAllGroup0(String regex, CharSequence content) { + return findAll(regex, content, 0); + } + + /** + * 取得内容中匹配的所有结果,获得匹配的所有结果中正则对应分组1的内容 + * + * @param regex 正则 + * @param content 被查找的内容 + * @return 结果列表 + * @since 3.1.2 + */ + public static List findAllGroup1(String regex, CharSequence content) { + return findAll(regex, content, 1); + } + + /** + * 取得内容中匹配的所有结果 + * + * @param regex 正则 + * @param content 被查找的内容 + * @param group 正则的分组 + * @return 结果列表 + * @since 3.0.6 + */ + public static List findAll(String regex, CharSequence content, int group) { + return findAll(regex, content, group, new ArrayList<>()); + } + + /** + * 取得内容中匹配的所有结果 + * + * @param 集合类型 + * @param regex 正则 + * @param content 被查找的内容 + * @param group 正则的分组 + * @param collection 返回的集合类型 + * @return 结果集 + */ + public static > T findAll(String regex, CharSequence content, int group, T collection) { + if (null == regex) { + return collection; + } + + return findAll(PatternPool.get(regex, Pattern.DOTALL), content, group, collection); + } + + /** + * 取得内容中匹配的所有结果,获得匹配的所有结果中正则对应分组0的内容 + * + * @param pattern 编译后的正则模式 + * @param content 被查找的内容 + * @return 结果列表 + * @since 3.1.2 + */ + public static List findAllGroup0(Pattern pattern, CharSequence content) { + return findAll(pattern, content, 0); + } + + /** + * 取得内容中匹配的所有结果,获得匹配的所有结果中正则对应分组1的内容 + * + * @param pattern 编译后的正则模式 + * @param content 被查找的内容 + * @return 结果列表 + * @since 3.1.2 + */ + public static List findAllGroup1(Pattern pattern, CharSequence content) { + return findAll(pattern, content, 1); + } + + /** + * 取得内容中匹配的所有结果 + * + * @param pattern 编译后的正则模式 + * @param content 被查找的内容 + * @param group 正则的分组 + * @return 结果列表 + * @since 3.0.6 + */ + public static List findAll(Pattern pattern, CharSequence content, int group) { + return findAll(pattern, content, group, new ArrayList<>()); + } + + /** + * 取得内容中匹配的所有结果 + * + * @param 集合类型 + * @param pattern 编译后的正则模式 + * @param content 被查找的内容 + * @param group 正则的分组 + * @param collection 返回的集合类型 + * @return 结果集 + */ + public static > T findAll(Pattern pattern, CharSequence content, int group, T collection) { + if (null == pattern || null == content) { + return null; + } + Assert.notNull(collection, "Collection must be not null !"); + + findAll(pattern, content, (matcher) -> collection.add(matcher.group(group))); + return collection; + } + + /** + * 取得内容中匹配的所有结果,使用{@link Consumer}完成匹配结果处理 + * + * @param pattern 编译后的正则模式 + * @param content 被查找的内容 + * @param consumer 匹配结果处理函数 + * @since 5.7.15 + */ + public static void findAll(Pattern pattern, CharSequence content, Consumer consumer) { + if (null == pattern || null == content) { + return; + } + + final Matcher matcher = pattern.matcher(content); + while (matcher.find()) { + consumer.accept(matcher); + } + } + + /** + * 计算指定字符串中,匹配pattern的个数 + * + * @param regex 正则表达式 + * @param content 被查找的内容 + * @return 匹配个数 + */ + public static int count(String regex, CharSequence content) { + if (null == regex || null == content) { + return 0; + } + + final Pattern pattern = PatternPool.get(regex, Pattern.DOTALL); + return count(pattern, content); + } + + /** + * 计算指定字符串中,匹配pattern的个数 + * + * @param pattern 编译后的正则模式 + * @param content 被查找的内容 + * @return 匹配个数 + */ + public static int count(Pattern pattern, CharSequence content) { + if (null == pattern || null == content) { + return 0; + } + + int count = 0; + final Matcher matcher = pattern.matcher(content); + while (matcher.find()) { + count++; + } + + return count; + } + + /** + * 指定内容中是否有表达式匹配的内容 + * + * @param regex 正则表达式 + * @param content 被查找的内容 + * @return 指定内容中是否有表达式匹配的内容 + * @since 3.3.1 + */ + public static boolean contains(String regex, CharSequence content) { + if (null == regex || null == content) { + return false; + } + + final Pattern pattern = PatternPool.get(regex, Pattern.DOTALL); + return contains(pattern, content); + } + + /** + * 指定内容中是否有表达式匹配的内容 + * + * @param pattern 编译后的正则模式 + * @param content 被查找的内容 + * @return 指定内容中是否有表达式匹配的内容 + * @since 3.3.1 + */ + public static boolean contains(Pattern pattern, CharSequence content) { + if (null == pattern || null == content) { + return false; + } + return pattern.matcher(content).find(); + } + + /** + * 找到指定正则匹配到字符串的开始位置 + * + * @param regex 正则 + * @param content 字符串 + * @return 位置,{@code null}表示未找到 + * @since 5.6.5 + */ + public static MatchResult indexOf(String regex, CharSequence content) { + if (null == regex || null == content) { + return null; + } + + final Pattern pattern = PatternPool.get(regex, Pattern.DOTALL); + return indexOf(pattern, content); + } + + /** + * 找到指定模式匹配到字符串的开始位置 + * + * @param pattern 模式 + * @param content 字符串 + * @return 位置,{@code null}表示未找到 + * @since 5.6.5 + */ + public static MatchResult indexOf(Pattern pattern, CharSequence content) { + if (null != pattern && null != content) { + final Matcher matcher = pattern.matcher(content); + if (matcher.find()) { + return matcher.toMatchResult(); + } + } + + return null; + } + + /** + * 找到指定正则匹配到第一个字符串的位置 + * + * @param regex 正则 + * @param content 字符串 + * @return 位置,{@code null}表示未找到 + * @since 5.6.5 + */ + public static MatchResult lastIndexOf(String regex, CharSequence content) { + if (null == regex || null == content) { + return null; + } + + final Pattern pattern = PatternPool.get(regex, Pattern.DOTALL); + return lastIndexOf(pattern, content); + } + + /** + * 找到指定模式匹配到最后一个字符串的位置 + * + * @param pattern 模式 + * @param content 字符串 + * @return 位置,{@code null}表示未找到 + * @since 5.6.5 + */ + public static MatchResult lastIndexOf(Pattern pattern, CharSequence content) { + MatchResult result = null; + if (null != pattern && null != content) { + final Matcher matcher = pattern.matcher(content); + while (matcher.find()) { + result = matcher.toMatchResult(); + } + } + + return result; + } + + /** + * 从字符串中获得第一个整数 + * + * @param StringWithNumber 带数字的字符串 + * @return 整数 + */ + public static Integer getFirstNumber(CharSequence StringWithNumber) { + return Convert.toInt(get(PatternPool.NUMBERS, StringWithNumber, 0), null); + } + + /** + * 给定内容是否匹配正则 + * + * @param regex 正则 + * @param content 内容 + * @return 正则为null或者""则不检查,返回true,内容为null返回false + */ + public static boolean isMatch(String regex, CharSequence content) { + if (content == null) { + // 提供null的字符串为不匹配 + return false; + } + + if (StrUtil.isEmpty(regex)) { + // 正则不存在则为全匹配 + return true; + } + + // Pattern pattern = Pattern.compile(regex, Pattern.DOTALL); + final Pattern pattern = PatternPool.get(regex, Pattern.DOTALL); + return isMatch(pattern, content); + } + + /** + * 给定内容是否匹配正则 + * + * @param pattern 模式 + * @param content 内容 + * @return 正则为null或者""则不检查,返回true,内容为null返回false + */ + public static boolean isMatch(Pattern pattern, CharSequence content) { + if (content == null || pattern == null) { + // 提供null的字符串为不匹配 + return false; + } + return pattern.matcher(content).matches(); + } + + /** + * 正则替换指定值
+ * 通过正则查找到字符串,然后把匹配到的字符串加入到replacementTemplate中,$1表示分组1的字符串 + * + *

+ * 例如:原字符串是:中文1234,我想把1234换成(1234),则可以: + * + *

+	 * ReUtil.replaceAll("中文1234", "(\\d+)", "($1)"))
+	 *
+	 * 结果:中文(1234)
+	 * 
+ * + * @param content 文本 + * @param regex 正则 + * @param replacementTemplate 替换的文本模板,可以使用$1类似的变量提取正则匹配出的内容 + * @return 处理后的文本 + */ + public static String replaceAll(CharSequence content, String regex, String replacementTemplate) { + final Pattern pattern = Pattern.compile(regex, Pattern.DOTALL); + return replaceAll(content, pattern, replacementTemplate); + } + + /** + * 正则替换指定值
+ * 通过正则查找到字符串,然后把匹配到的字符串加入到replacementTemplate中,$1表示分组1的字符串 + * + * @param content 文本 + * @param pattern {@link Pattern} + * @param replacementTemplate 替换的文本模板,可以使用$1类似的变量提取正则匹配出的内容 + * @return 处理后的文本 + * @since 3.0.4 + */ + public static String replaceAll(CharSequence content, Pattern pattern, String replacementTemplate) { + if (StrUtil.isEmpty(content)) { + return StrUtil.str(content); + } + + final Matcher matcher = pattern.matcher(content); + boolean result = matcher.find(); + if (result) { + final Set varNums = findAll(PatternPool.GROUP_VAR, replacementTemplate, 1, new TreeSet<>(LengthComparator.INSTANCE.reversed())); + final StringBuffer sb = new StringBuffer(); + do { + String replacement = replacementTemplate; + for (final String var : varNums) { + final int group = Integer.parseInt(var); + replacement = replacement.replace("$" + var, matcher.group(group)); + } + matcher.appendReplacement(sb, escape(replacement)); + result = matcher.find(); + } while (result); + matcher.appendTail(sb); + return sb.toString(); + } + return StrUtil.str(content); + } + + /** + * 替换所有正则匹配的文本,并使用自定义函数决定如何替换
+ * replaceFun可以通过{@link Matcher}提取出匹配到的内容的不同部分,然后经过重新处理、组装变成新的内容放回原位。 + * + *
+	 *     replaceAll(this.content, "(\\d+)", parameters -> "-" + parameters.group(1) + "-")
+	 *     // 结果为:"ZZZaaabbbccc中文-1234-"
+	 * 
+ * + * @param str 要替换的字符串 + * @param regex 用于匹配的正则式 + * @param replaceFun 决定如何替换的函数 + * @return 替换后的文本 + * @since 4.2.2 + */ + public static String replaceAll(CharSequence str, String regex, Func1 replaceFun) { + return replaceAll(str, Pattern.compile(regex), replaceFun); + } + + /** + * 替换所有正则匹配的文本,并使用自定义函数决定如何替换
+ * replaceFun可以通过{@link Matcher}提取出匹配到的内容的不同部分,然后经过重新处理、组装变成新的内容放回原位。 + * + *
+	 *     replaceAll(this.content, "(\\d+)", parameters -> "-" + parameters.group(1) + "-")
+	 *     // 结果为:"ZZZaaabbbccc中文-1234-"
+	 * 
+ * + * @param str 要替换的字符串 + * @param pattern 用于匹配的正则式 + * @param replaceFun 决定如何替换的函数,可能被多次调用(当有多个匹配时) + * @return 替换后的字符串 + * @since 4.2.2 + */ + public static String replaceAll(CharSequence str, Pattern pattern, Func1 replaceFun) { + if (StrUtil.isEmpty(str)) { + return StrUtil.str(str); + } + + final Matcher matcher = pattern.matcher(str); + final StringBuffer buffer = new StringBuffer(); + while (matcher.find()) { + try { + matcher.appendReplacement(buffer, replaceFun.call(matcher)); + } catch (Exception e) { + throw new UtilException(e); + } + } + matcher.appendTail(buffer); + return buffer.toString(); + } + + /** + * 转义字符,将正则的关键字转义 + * + * @param c 字符 + * @return 转义后的文本 + */ + public static String escape(char c) { + final StringBuilder builder = new StringBuilder(); + if (RE_KEYS.contains(c)) { + builder.append('\\'); + } + builder.append(c); + return builder.toString(); + } + + /** + * 转义字符串,将正则的关键字转义 + * + * @param content 文本 + * @return 转义后的文本 + */ + public static String escape(CharSequence content) { + if (StrUtil.isBlank(content)) { + return StrUtil.str(content); + } + + final StringBuilder builder = new StringBuilder(); + int len = content.length(); + char current; + for (int i = 0; i < len; i++) { + current = content.charAt(i); + if (RE_KEYS.contains(current)) { + builder.append('\\'); + } + builder.append(current); + } + return builder.toString(); + } +} diff --git a/src/main/java/cn/hutool/core/util/ReferenceUtil.java b/src/main/java/cn/hutool/core/util/ReferenceUtil.java new file mode 100644 index 0000000..e4b13b6 --- /dev/null +++ b/src/main/java/cn/hutool/core/util/ReferenceUtil.java @@ -0,0 +1,75 @@ +package cn.hutool.core.util; + +import java.lang.ref.PhantomReference; +import java.lang.ref.Reference; +import java.lang.ref.ReferenceQueue; +import java.lang.ref.SoftReference; +import java.lang.ref.WeakReference; + +/** + * 引用工具类,主要针对{@link Reference} 工具化封装
+ * 主要封装包括: + *
+ * 1. {@link SoftReference} 软引用,在GC报告内存不足时会被GC回收
+ * 2. {@link WeakReference} 弱引用,在GC时发现弱引用会回收其对象
+ * 3. {@link PhantomReference} 虚引用,在GC时发现虚引用对象,会将{@link PhantomReference}插入{@link ReferenceQueue}。 此时对象未被真正回收,要等到{@link ReferenceQueue}被真正处理后才会被回收。
+ * 
+ * + * @author looly + * @since 3.1.2 + */ +public class ReferenceUtil { + + /** + * 获得引用 + * + * @param 被引用对象类型 + * @param type 引用类型枚举 + * @param referent 被引用对象 + * @return {@link Reference} + */ + public static Reference create(ReferenceType type, T referent) { + return create(type, referent, null); + } + + /** + * 获得引用 + * + * @param 被引用对象类型 + * @param type 引用类型枚举 + * @param referent 被引用对象 + * @param queue 引用队列 + * @return {@link Reference} + */ + public static Reference create(ReferenceType type, T referent, ReferenceQueue queue) { + switch (type) { + case SOFT: + return new SoftReference<>(referent, queue); + case WEAK: + return new WeakReference<>(referent, queue); + case PHANTOM: + return new PhantomReference<>(referent, queue); + default: + return null; + } + } + + /** + * 引用类型 + * + * @author looly + * + */ + public enum ReferenceType { + /** 软引用,在GC报告内存不足时会被GC回收 */ + SOFT, + /** 弱引用,在GC时发现弱引用会回收其对象 */ + WEAK, + /** + * 虚引用,在GC时发现虚引用对象,会将{@link PhantomReference}插入{@link ReferenceQueue}。
+ * 此时对象未被真正回收,要等到{@link ReferenceQueue}被真正处理后才会被回收。 + */ + PHANTOM + } + +} diff --git a/src/main/java/cn/hutool/core/util/ReflectUtil.java b/src/main/java/cn/hutool/core/util/ReflectUtil.java new file mode 100644 index 0000000..ffa6afa --- /dev/null +++ b/src/main/java/cn/hutool/core/util/ReflectUtil.java @@ -0,0 +1,1194 @@ +package cn.hutool.core.util; + +import cn.hutool.core.annotation.Alias; +import cn.hutool.core.bean.NullWrapperBean; +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.collection.UniqueKeySet; +import cn.hutool.core.convert.Convert; +import cn.hutool.core.exceptions.InvocationTargetRuntimeException; +import cn.hutool.core.exceptions.UtilException; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.lang.Filter; +import cn.hutool.core.lang.reflect.MethodHandleUtil; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.map.WeakConcurrentMap; + +import java.lang.reflect.AccessibleObject; +import java.lang.reflect.Array; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.AbstractMap; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * 反射工具类 + * + * @author Looly + * @since 3.0.9 + */ +public class ReflectUtil { + + /** + * 构造对象缓存 + */ + private static final WeakConcurrentMap, Constructor[]> CONSTRUCTORS_CACHE = new WeakConcurrentMap<>(); + /** + * 字段缓存 + */ + private static final WeakConcurrentMap, Field[]> FIELDS_CACHE = new WeakConcurrentMap<>(); + /** + * 方法缓存 + */ + private static final WeakConcurrentMap, Method[]> METHODS_CACHE = new WeakConcurrentMap<>(); + + // --------------------------------------------------------------------------------------------------------- Constructor + + /** + * 查找类中的指定参数的构造方法,如果找到构造方法,会自动设置可访问为true + * + * @param 对象类型 + * @param clazz 类 + * @param parameterTypes 参数类型,只要任何一个参数是指定参数的父类或接口或相等即可,此参数可以不传 + * @return 构造方法,如果未找到返回null + */ + @SuppressWarnings("unchecked") + public static Constructor getConstructor(Class clazz, Class... parameterTypes) { + if (null == clazz) { + return null; + } + + final Constructor[] constructors = getConstructors(clazz); + Class[] pts; + for (Constructor constructor : constructors) { + pts = constructor.getParameterTypes(); + if (ClassUtil.isAllAssignableFrom(pts, parameterTypes)) { + // 构造可访问 + setAccessible(constructor); + return (Constructor) constructor; + } + } + return null; + } + + /** + * 获得一个类中所有构造列表 + * + * @param 构造的对象类型 + * @param beanClass 类,非{@code null} + * @return 字段列表 + * @throws SecurityException 安全检查异常 + */ + @SuppressWarnings("unchecked") + public static Constructor[] getConstructors(Class beanClass) throws SecurityException { + Assert.notNull(beanClass); + return (Constructor[]) CONSTRUCTORS_CACHE.computeIfAbsent(beanClass, () -> getConstructorsDirectly(beanClass)); + } + + /** + * 获得一个类中所有构造列表,直接反射获取,无缓存 + * + * @param beanClass 类 + * @return 字段列表 + * @throws SecurityException 安全检查异常 + */ + public static Constructor[] getConstructorsDirectly(Class beanClass) throws SecurityException { + return beanClass.getDeclaredConstructors(); + } + + // --------------------------------------------------------------------------------------------------------- Field + + /** + * 查找指定类中是否包含指定名称对应的字段,包括所有字段(包括非public字段),也包括父类和Object类的字段 + * + * @param beanClass 被查找字段的类,不能为null + * @param name 字段名 + * @return 是否包含字段 + * @throws SecurityException 安全异常 + * @since 4.1.21 + */ + public static boolean hasField(Class beanClass, String name) throws SecurityException { + return null != getField(beanClass, name); + } + + /** + * 获取字段名,如果存在{@link Alias}注解,读取注解的值作为名称 + * + * @param field 字段 + * @return 字段名 + * @since 5.1.6 + */ + public static String getFieldName(Field field) { + if (null == field) { + return null; + } + + final Alias alias = field.getAnnotation(Alias.class); + if (null != alias) { + return alias.value(); + } + + return field.getName(); + } + + /** + * 查找指定类中的指定name的字段(包括非public字段),也包括父类和Object类的字段, 字段不存在则返回{@code null} + * + * @param beanClass 被查找字段的类,不能为null + * @param name 字段名 + * @return 字段 + * @throws SecurityException 安全异常 + */ + public static Field getField(Class beanClass, String name) throws SecurityException { + final Field[] fields = getFields(beanClass); + return ArrayUtil.firstMatch((field) -> name.equals(getFieldName(field)), fields); + } + + /** + * 获取指定类中字段名和字段对应的有序Map,包括其父类中的字段
+ * 如果子类与父类中存在同名字段,则这两个字段同时存在,子类字段在前,父类字段在后。 + * + * @param beanClass 类 + * @return 字段名和字段对应的Map,有序 + * @since 5.0.7 + */ + public static Map getFieldMap(Class beanClass) { + final Field[] fields = getFields(beanClass); + final HashMap map = MapUtil.newHashMap(fields.length, true); + for (Field field : fields) { + map.put(field.getName(), field); + } + return map; + } + + /** + * 获得一个类中所有字段列表,包括其父类中的字段
+ * 如果子类与父类中存在同名字段,则这两个字段同时存在,子类字段在前,父类字段在后。 + * + * @param beanClass 类 + * @return 字段列表 + * @throws SecurityException 安全检查异常 + */ + public static Field[] getFields(Class beanClass) throws SecurityException { + Assert.notNull(beanClass); + return FIELDS_CACHE.computeIfAbsent(beanClass, () -> getFieldsDirectly(beanClass, true)); + } + + + /** + * 获得一个类中所有满足条件的字段列表,包括其父类中的字段
+ * 如果子类与父类中存在同名字段,则这两个字段同时存在,子类字段在前,父类字段在后。 + * + * @param beanClass 类 + * @param fieldFilter field过滤器,过滤掉不需要的field + * @return 字段列表 + * @throws SecurityException 安全检查异常 + * @since 5.7.14 + */ + public static Field[] getFields(Class beanClass, Filter fieldFilter) throws SecurityException { + return ArrayUtil.filter(getFields(beanClass), fieldFilter); + } + + /** + * 获得一个类中所有字段列表,直接反射获取,无缓存
+ * 如果子类与父类中存在同名字段,则这两个字段同时存在,子类字段在前,父类字段在后。 + * + * @param beanClass 类 + * @param withSuperClassFields 是否包括父类的字段列表 + * @return 字段列表 + * @throws SecurityException 安全检查异常 + */ + public static Field[] getFieldsDirectly(Class beanClass, boolean withSuperClassFields) throws SecurityException { + Assert.notNull(beanClass); + + Field[] allFields = null; + Class searchType = beanClass; + Field[] declaredFields; + while (searchType != null) { + declaredFields = searchType.getDeclaredFields(); + if (null == allFields) { + allFields = declaredFields; + } else { + allFields = ArrayUtil.append(allFields, declaredFields); + } + searchType = withSuperClassFields ? searchType.getSuperclass() : null; + } + + return allFields; + } + + /** + * 获取字段值 + * + * @param obj 对象,如果static字段,此处为类 + * @param fieldName 字段名 + * @return 字段值 + * @throws UtilException 包装IllegalAccessException异常 + */ + public static Object getFieldValue(Object obj, String fieldName) throws UtilException { + if (null == obj || StrUtil.isBlank(fieldName)) { + return null; + } + return getFieldValue(obj, getField(obj instanceof Class ? (Class) obj : obj.getClass(), fieldName)); + } + + /** + * 获取静态字段值 + * + * @param field 字段 + * @return 字段值 + * @throws UtilException 包装IllegalAccessException异常 + * @since 5.1.0 + */ + public static Object getStaticFieldValue(Field field) throws UtilException { + return getFieldValue(null, field); + } + + /** + * 获取字段值 + * + * @param obj 对象,static字段则此字段为null + * @param field 字段 + * @return 字段值 + * @throws UtilException 包装IllegalAccessException异常 + */ + public static Object getFieldValue(Object obj, Field field) throws UtilException { + if (null == field) { + return null; + } + if (obj instanceof Class) { + // 静态字段获取时对象为null + obj = null; + } + + setAccessible(field); + Object result; + try { + result = field.get(obj); + } catch (IllegalAccessException e) { + throw new UtilException(e, "IllegalAccess for {}.{}", field.getDeclaringClass(), field.getName()); + } + return result; + } + + /** + * 获取所有字段的值 + * + * @param obj bean对象,如果是static字段,此处为类class + * @return 字段值数组 + * @since 4.1.17 + */ + public static Object[] getFieldsValue(Object obj) { + if (null != obj) { + final Field[] fields = getFields(obj instanceof Class ? (Class) obj : obj.getClass()); + if (null != fields) { + final Object[] values = new Object[fields.length]; + for (int i = 0; i < fields.length; i++) { + values[i] = getFieldValue(obj, fields[i]); + } + return values; + } + } + return null; + } + + /** + * 设置字段值
+ * 若值类型与字段类型不一致,则会尝试通过 {@link Convert} 进行转换
+ * 若字段类型是原始类型而传入的值是 null,则会将字段设置为对应原始类型的默认值(见 {@link ClassUtil#getDefaultValue(Class)}) + * 如果是final字段,setFieldValue,调用这可以先调用 {@link ReflectUtil#removeFinalModify(Field)}方法去除final修饰符
+ * + * @param obj 对象,static字段则此处传Class + * @param fieldName 字段名 + * @param value 值,当值类型与字段类型不匹配时,会尝试转换 + * @throws UtilException 包装IllegalAccessException异常 + */ + public static void setFieldValue(Object obj, String fieldName, Object value) throws UtilException { + Assert.notNull(obj); + Assert.notBlank(fieldName); + + final Field field = getField((obj instanceof Class) ? (Class) obj : obj.getClass(), fieldName); + Assert.notNull(field, "Field [{}] is not exist in [{}]", fieldName, obj.getClass().getName()); + setFieldValue(obj, field, value); + } + + /** + * 设置字段值
+ * 若值类型与字段类型不一致,则会尝试通过 {@link Convert} 进行转换
+ * 若字段类型是原始类型而传入的值是 null,则会将字段设置为对应原始类型的默认值(见 {@link ClassUtil#getDefaultValue(Class)})
+ * 如果是final字段,setFieldValue,调用这可以先调用 {@link ReflectUtil#removeFinalModify(Field)}方法去除final修饰符 + * + * @param obj 对象,如果是static字段,此参数为null + * @param field 字段 + * @param value 值,当值类型与字段类型不匹配时,会尝试转换 + * @throws UtilException UtilException 包装IllegalAccessException异常 + */ + public static void setFieldValue(Object obj, Field field, Object value) throws UtilException { + Assert.notNull(field, "Field in [{}] not exist !", obj); + + final Class fieldType = field.getType(); + if (null != value) { + if (!fieldType.isAssignableFrom(value.getClass())) { + //对于类型不同的字段,尝试转换,转换失败则使用原对象类型 + final Object targetValue = Convert.convert(fieldType, value); + if (null != targetValue) { + value = targetValue; + } + } + } else { + // 获取null对应默认值,防止原始类型造成空指针问题 + value = ClassUtil.getDefaultValue(fieldType); + } + + setAccessible(field); + try { + field.set(obj instanceof Class ? null : obj, value); + } catch (IllegalAccessException e) { + throw new UtilException(e, "IllegalAccess for {}.{}", obj, field.getName()); + } + } + + /** + * 是否为父类引用字段
+ * 当字段所在类是对象子类时(对象中定义的非static的class),会自动生成一个以"this$0"为名称的字段,指向父类对象 + * + * @param field 字段 + * @return 是否为父类引用字段 + * @since 5.7.20 + */ + public static boolean isOuterClassField(Field field) { + return "this$0".equals(field.getName()); + } + + // --------------------------------------------------------------------------------------------------------- method + + /** + * 获得指定类本类及其父类中的Public方法名
+ * 去重重载的方法 + * + * @param clazz 类 + * @return 方法名Set + */ + public static Set getPublicMethodNames(Class clazz) { + final HashSet methodSet = new HashSet<>(); + final Method[] methodArray = getPublicMethods(clazz); + if (ArrayUtil.isNotEmpty(methodArray)) { + for (Method method : methodArray) { + methodSet.add(method.getName()); + } + } + return methodSet; + } + + /** + * 获得本类及其父类所有Public方法 + * + * @param clazz 查找方法的类 + * @return 过滤后的方法列表 + */ + public static Method[] getPublicMethods(Class clazz) { + return null == clazz ? null : clazz.getMethods(); + } + + /** + * 获得指定类过滤后的Public方法列表
+ * TODO 6.x此方法更改返回Method[] + * + * @param clazz 查找方法的类 + * @param filter 过滤器 + * @return 过滤后的方法列表 + */ + public static List getPublicMethods(Class clazz, Filter filter) { + if (null == clazz) { + return null; + } + + final Method[] methods = getPublicMethods(clazz); + List methodList; + if (null != filter) { + methodList = new ArrayList<>(); + for (Method method : methods) { + if (filter.accept(method)) { + methodList.add(method); + } + } + } else { + methodList = CollUtil.newArrayList(methods); + } + return methodList; + } + + /** + * 获得指定类过滤后的Public方法列表 + * + * @param clazz 查找方法的类 + * @param excludeMethods 不包括的方法 + * @return 过滤后的方法列表 + */ + public static List getPublicMethods(Class clazz, Method... excludeMethods) { + final HashSet excludeMethodSet = CollUtil.newHashSet(excludeMethods); + return getPublicMethods(clazz, method -> !excludeMethodSet.contains(method)); + } + + /** + * 获得指定类过滤后的Public方法列表 + * + * @param clazz 查找方法的类 + * @param excludeMethodNames 不包括的方法名列表 + * @return 过滤后的方法列表 + */ + public static List getPublicMethods(Class clazz, String... excludeMethodNames) { + final HashSet excludeMethodNameSet = CollUtil.newHashSet(excludeMethodNames); + return getPublicMethods(clazz, method -> !excludeMethodNameSet.contains(method.getName())); + } + + /** + * 查找指定Public方法 如果找不到对应的方法或方法不为public的则返回{@code null} + * + * @param clazz 类 + * @param methodName 方法名 + * @param paramTypes 参数类型 + * @return 方法 + * @throws SecurityException 无权访问抛出异常 + */ + public static Method getPublicMethod(Class clazz, String methodName, Class... paramTypes) throws SecurityException { + try { + return clazz.getMethod(methodName, paramTypes); + } catch (NoSuchMethodException ex) { + return null; + } + } + + /** + * 查找指定对象中的所有方法(包括非public方法),也包括父对象和Object类的方法 + * + *

+ * 此方法为精准获取方法名,即方法名和参数数量和类型必须一致,否则返回{@code null}。 + *

+ * + * @param obj 被查找的对象,如果为{@code null}返回{@code null} + * @param methodName 方法名,如果为空字符串返回{@code null} + * @param args 参数 + * @return 方法 + * @throws SecurityException 无访问权限抛出异常 + */ + public static Method getMethodOfObj(Object obj, String methodName, Object... args) throws SecurityException { + if (null == obj || StrUtil.isBlank(methodName)) { + return null; + } + return getMethod(obj.getClass(), methodName, ClassUtil.getClasses(args)); + } + + /** + * 忽略大小写查找指定方法,如果找不到对应的方法则返回{@code null} + * + *

+ * 此方法为精准获取方法名,即方法名和参数数量和类型必须一致,否则返回{@code null}。 + *

+ * + * @param clazz 类,如果为{@code null}返回{@code null} + * @param methodName 方法名,如果为空字符串返回{@code null} + * @param paramTypes 参数类型,指定参数类型如果是方法的子类也算 + * @return 方法 + * @throws SecurityException 无权访问抛出异常 + * @since 3.2.0 + */ + public static Method getMethodIgnoreCase(Class clazz, String methodName, Class... paramTypes) throws SecurityException { + return getMethod(clazz, true, methodName, paramTypes); + } + + /** + * 查找指定方法 如果找不到对应的方法则返回{@code null} + * + *

+ * 此方法为精准获取方法名,即方法名和参数数量和类型必须一致,否则返回{@code null}。 + *

+ * + * @param clazz 类,如果为{@code null}返回{@code null} + * @param methodName 方法名,如果为空字符串返回{@code null} + * @param paramTypes 参数类型,指定参数类型如果是方法的子类也算 + * @return 方法 + * @throws SecurityException 无权访问抛出异常 + */ + public static Method getMethod(Class clazz, String methodName, Class... paramTypes) throws SecurityException { + return getMethod(clazz, false, methodName, paramTypes); + } + + /** + * 查找指定方法 如果找不到对应的方法则返回{@code null}
+ * 此方法为精准获取方法名,即方法名和参数数量和类型必须一致,否则返回{@code null}。
+ * 如果查找的方法有多个同参数类型重载,查找第一个找到的方法 + * + * @param clazz 类,如果为{@code null}返回{@code null} + * @param ignoreCase 是否忽略大小写 + * @param methodName 方法名,如果为空字符串返回{@code null} + * @param paramTypes 参数类型,指定参数类型如果是方法的子类也算 + * @return 方法 + * @throws SecurityException 无权访问抛出异常 + * @since 3.2.0 + */ + public static Method getMethod(Class clazz, boolean ignoreCase, String methodName, Class... paramTypes) throws SecurityException { + if (null == clazz || StrUtil.isBlank(methodName)) { + return null; + } + + Method res = null; + final Method[] methods = getMethods(clazz); + if (ArrayUtil.isNotEmpty(methods)) { + for (Method method : methods) { + if (StrUtil.equals(methodName, method.getName(), ignoreCase) + && ClassUtil.isAllAssignableFrom(method.getParameterTypes(), paramTypes) + //排除协变桥接方法,pr#1965@Github + && (res == null + || res.getReturnType().isAssignableFrom(method.getReturnType()))) { + res = method; + } + } + } + return res; + } + + /** + * 按照方法名查找指定方法名的方法,只返回匹配到的第一个方法,如果找不到对应的方法则返回{@code null} + * + *

+ * 此方法只检查方法名是否一致,并不检查参数的一致性。 + *

+ * + * @param clazz 类,如果为{@code null}返回{@code null} + * @param methodName 方法名,如果为空字符串返回{@code null} + * @return 方法 + * @throws SecurityException 无权访问抛出异常 + * @since 4.3.2 + */ + public static Method getMethodByName(Class clazz, String methodName) throws SecurityException { + return getMethodByName(clazz, false, methodName); + } + + /** + * 按照方法名查找指定方法名的方法,只返回匹配到的第一个方法,如果找不到对应的方法则返回{@code null} + * + *

+ * 此方法只检查方法名是否一致(忽略大小写),并不检查参数的一致性。 + *

+ * + * @param clazz 类,如果为{@code null}返回{@code null} + * @param methodName 方法名,如果为空字符串返回{@code null} + * @return 方法 + * @throws SecurityException 无权访问抛出异常 + * @since 4.3.2 + */ + public static Method getMethodByNameIgnoreCase(Class clazz, String methodName) throws SecurityException { + return getMethodByName(clazz, true, methodName); + } + + /** + * 按照方法名查找指定方法名的方法,只返回匹配到的第一个方法,如果找不到对应的方法则返回{@code null} + * + *

+ * 此方法只检查方法名是否一致,并不检查参数的一致性。 + *

+ * + * @param clazz 类,如果为{@code null}返回{@code null} + * @param ignoreCase 是否忽略大小写 + * @param methodName 方法名,如果为空字符串返回{@code null} + * @return 方法 + * @throws SecurityException 无权访问抛出异常 + * @since 4.3.2 + */ + public static Method getMethodByName(Class clazz, boolean ignoreCase, String methodName) throws SecurityException { + if (null == clazz || StrUtil.isBlank(methodName)) { + return null; + } + + Method res = null; + final Method[] methods = getMethods(clazz); + if (ArrayUtil.isNotEmpty(methods)) { + for (Method method : methods) { + if (StrUtil.equals(methodName, method.getName(), ignoreCase) + //排除协变桥接方法,pr#1965@Github + && (res == null + || res.getReturnType().isAssignableFrom(method.getReturnType()))) { + res = method; + } + } + } + return res; + } + + /** + * 获得指定类中的方法名
+ * 去重重载的方法 + * + * @param clazz 类 + * @return 方法名Set + * @throws SecurityException 安全异常 + */ + public static Set getMethodNames(Class clazz) throws SecurityException { + final HashSet methodSet = new HashSet<>(); + final Method[] methods = getMethods(clazz); + for (Method method : methods) { + methodSet.add(method.getName()); + } + return methodSet; + } + + /** + * 获得指定类过滤后的方法列表 + * + * @param clazz 查找方法的类 + * @param filter 过滤器 + * @return 过滤后的方法列表 + * @throws SecurityException 安全异常 + */ + public static Method[] getMethods(Class clazz, Filter filter) throws SecurityException { + if (null == clazz) { + return null; + } + return ArrayUtil.filter(getMethods(clazz), filter); + } + + /** + * 获得一个类中所有方法列表,包括其父类中的方法 + * + * @param beanClass 类,非{@code null} + * @return 方法列表 + * @throws SecurityException 安全检查异常 + */ + public static Method[] getMethods(Class beanClass) throws SecurityException { + Assert.notNull(beanClass); + return METHODS_CACHE.computeIfAbsent(beanClass, + () -> getMethodsDirectly(beanClass, true, true)); + } + + /** + * 获得一个类中所有方法列表,直接反射获取,无缓存
+ * 接口获取方法和默认方法,获取的方法包括: + *
    + *
  • 本类中的所有方法(包括static方法)
  • + *
  • 父类中的所有方法(包括static方法)
  • + *
  • Object中(包括static方法)
  • + *
+ * + * @param beanClass 类或接口 + * @param withSupers 是否包括父类或接口的方法列表 + * @param withMethodFromObject 是否包括Object中的方法 + * @return 方法列表 + * @throws SecurityException 安全检查异常 + */ + public static Method[] getMethodsDirectly(Class beanClass, boolean withSupers, boolean withMethodFromObject) throws SecurityException { + Assert.notNull(beanClass); + + if (beanClass.isInterface()) { + // 对于接口,直接调用Class.getMethods方法获取所有方法,因为接口都是public方法 + return withSupers ? beanClass.getMethods() : beanClass.getDeclaredMethods(); + } + + final UniqueKeySet result = new UniqueKeySet<>(true, ReflectUtil::getUniqueKey); + Class searchType = beanClass; + while (searchType != null) { + if (!withMethodFromObject && Object.class == searchType) { + break; + } + result.addAllIfAbsent(Arrays.asList(searchType.getDeclaredMethods())); + result.addAllIfAbsent(getDefaultMethodsFromInterface(searchType)); + + + searchType = (withSupers && !searchType.isInterface()) ? searchType.getSuperclass() : null; + } + + return result.toArray(new Method[0]); + } + + /** + * 是否为equals方法 + * + * @param method 方法 + * @return 是否为equals方法 + */ + public static boolean isEqualsMethod(Method method) { + if (method == null || + 1 != method.getParameterCount() || + !"equals".equals(method.getName())) { + return false; + } + return (method.getParameterTypes()[0] == Object.class); + } + + /** + * 是否为hashCode方法 + * + * @param method 方法 + * @return 是否为hashCode方法 + */ + public static boolean isHashCodeMethod(Method method) { + return method != null// + && "hashCode".equals(method.getName())// + && isEmptyParam(method); + } + + /** + * 是否为toString方法 + * + * @param method 方法 + * @return 是否为toString方法 + */ + public static boolean isToStringMethod(Method method) { + return method != null// + && "toString".equals(method.getName())// + && isEmptyParam(method); + } + + /** + * 是否为无参数方法 + * + * @param method 方法 + * @return 是否为无参数方法 + * @since 5.1.1 + */ + public static boolean isEmptyParam(Method method) { + return method.getParameterCount() == 0; + } + + /** + * 检查给定方法是否为Getter或者Setter方法,规则为:
+ *
    + *
  • 方法参数必须为0个或1个
  • + *
  • 如果是无参方法,则判断是否以“get”或“is”开头
  • + *
  • 如果方法参数1个,则判断是否以“set”开头
  • + *
+ * + * @param method 方法 + * @return 是否为Getter或者Setter方法 + * @since 5.7.20 + */ + public static boolean isGetterOrSetterIgnoreCase(Method method) { + return isGetterOrSetter(method, true); + } + + /** + * 检查给定方法是否为Getter或者Setter方法,规则为:
+ *
    + *
  • 方法参数必须为0个或1个
  • + *
  • 方法名称不能是getClass
  • + *
  • 如果是无参方法,则判断是否以“get”或“is”开头
  • + *
  • 如果方法参数1个,则判断是否以“set”开头
  • + *
+ * + * @param method 方法 + * @param ignoreCase 是否忽略方法名的大小写 + * @return 是否为Getter或者Setter方法 + * @since 5.7.20 + */ + public static boolean isGetterOrSetter(Method method, boolean ignoreCase) { + if (null == method) { + return false; + } + + // 参数个数必须为0或1 + final int parameterCount = method.getParameterCount(); + if (parameterCount > 1) { + return false; + } + + String name = method.getName(); + // 跳过getClass这个特殊方法 + if ("getClass".equals(name)) { + return false; + } + if (ignoreCase) { + name = name.toLowerCase(); + } + switch (parameterCount) { + case 0: + return name.startsWith("get") || name.startsWith("is"); + case 1: + return name.startsWith("set"); + default: + return false; + } + } + // --------------------------------------------------------------------------------------------------------- newInstance + + /** + * 实例化对象 + * + * @param 对象类型 + * @param clazz 类名 + * @return 对象 + * @throws UtilException 包装各类异常 + */ + @SuppressWarnings("unchecked") + public static T newInstance(String clazz) throws UtilException { + try { + return (T) Class.forName(clazz).newInstance(); + } catch (Exception e) { + throw new UtilException(e, "Instance class [{}] error!", clazz); + } + } + + /** + * 实例化对象 + * + * @param 对象类型 + * @param clazz 类 + * @param params 构造函数参数 + * @return 对象 + * @throws UtilException 包装各类异常 + */ + public static T newInstance(Class clazz, Object... params) throws UtilException { + if (ArrayUtil.isEmpty(params)) { + final Constructor constructor = getConstructor(clazz); + if (null == constructor) { + throw new UtilException("No constructor for [{}]", clazz); + } + try { + return constructor.newInstance(); + } catch (Exception e) { + throw new UtilException(e, "Instance class [{}] error!", clazz); + } + } + + final Class[] paramTypes = ClassUtil.getClasses(params); + final Constructor constructor = getConstructor(clazz, paramTypes); + if (null == constructor) { + throw new UtilException("No Constructor matched for parameter types: [{}]", new Object[]{paramTypes}); + } + try { + return constructor.newInstance(params); + } catch (Exception e) { + throw new UtilException(e, "Instance class [{}] error!", clazz); + } + } + + /** + * 尝试遍历并调用此类的所有构造方法,直到构造成功并返回 + *

+ * 对于某些特殊的接口,按照其默认实现实例化,例如: + *

+	 *     Map       -》 HashMap
+	 *     Collction -》 ArrayList
+	 *     List      -》 ArrayList
+	 *     Set       -》 HashSet
+	 * 
+ * + * @param 对象类型 + * @param type 被构造的类 + * @return 构造后的对象,构造失败返回{@code null} + */ + @SuppressWarnings("unchecked") + public static T newInstanceIfPossible(Class type) { + Assert.notNull(type); + + // 原始类型 + if (type.isPrimitive()) { + return (T) ClassUtil.getPrimitiveDefaultValue(type); + } + + // 某些特殊接口的实例化按照默认实现进行 + if (type.isAssignableFrom(AbstractMap.class)) { + type = (Class) HashMap.class; + } else if (type.isAssignableFrom(List.class)) { + type = (Class) ArrayList.class; + } else if (type.isAssignableFrom(Set.class)) { + type = (Class) HashSet.class; + } + + try { + return newInstance(type); + } catch (Exception e) { + // ignore + // 默认构造不存在的情况下查找其它构造 + } + + // 枚举 + if (type.isEnum()) { + return type.getEnumConstants()[0]; + } + + // 数组 + if (type.isArray()) { + return (T) Array.newInstance(type.getComponentType(), 0); + } + + final Constructor[] constructors = getConstructors(type); + Class[] parameterTypes; + for (Constructor constructor : constructors) { + parameterTypes = constructor.getParameterTypes(); + if (0 == parameterTypes.length) { + continue; + } + setAccessible(constructor); + try { + return constructor.newInstance(ClassUtil.getDefaultValues(parameterTypes)); + } catch (Exception ignore) { + // 构造出错时继续尝试下一种构造方式 + } + } + return null; + } + + // --------------------------------------------------------------------------------------------------------- invoke + + /** + * 执行静态方法 + * + * @param 对象类型 + * @param method 方法(对象方法或static方法都可) + * @param args 参数对象 + * @return 结果 + * @throws UtilException 多种异常包装 + */ + public static T invokeStatic(Method method, Object... args) throws UtilException { + return invoke(null, method, args); + } + + /** + * 执行方法
+ * 执行前要检查给定参数: + * + *
+	 * 1. 参数个数是否与方法参数个数一致
+	 * 2. 如果某个参数为null但是方法这个位置的参数为原始类型,则赋予原始类型默认值
+	 * 
+ * + * @param 返回对象类型 + * @param obj 对象,如果执行静态方法,此值为{@code null} + * @param method 方法(对象方法或static方法都可) + * @param args 参数对象 + * @return 结果 + * @throws UtilException 一些列异常的包装 + */ + public static T invokeWithCheck(Object obj, Method method, Object... args) throws UtilException { + final Class[] types = method.getParameterTypes(); + if (null != args) { + Assert.isTrue(args.length == types.length, "Params length [{}] is not fit for param length [{}] of method !", args.length, types.length); + Class type; + for (int i = 0; i < args.length; i++) { + type = types[i]; + if (type.isPrimitive() && null == args[i]) { + // 参数是原始类型,而传入参数为null时赋予默认值 + args[i] = ClassUtil.getDefaultValue(type); + } + } + } + + return invoke(obj, method, args); + } + + /** + * 执行方法 + * + *

+ * 对于用户传入参数会做必要检查,包括: + * + *

+	 *     1、忽略多余的参数
+	 *     2、参数不够补齐默认值
+	 *     3、传入参数为null,但是目标参数类型为原始类型,做转换
+	 * 
+ * + * @param 返回对象类型 + * @param obj 对象,如果执行静态方法,此值为{@code null} + * @param method 方法(对象方法或static方法都可) + * @param args 参数对象 + * @return 结果 + * @throws InvocationTargetRuntimeException 目标方法执行异常 + * @throws UtilException {@link IllegalAccessException}异常的包装 + */ + public static T invoke(Object obj, Method method, Object... args) throws InvocationTargetRuntimeException, UtilException { + try { + return invokeRaw(obj, method, args); + } catch (InvocationTargetException e) { + throw new InvocationTargetRuntimeException(e); + } catch (IllegalAccessException e) { + throw new UtilException(e); + } + } + + /** + * 执行方法 + * + *

+ * 对于用户传入参数会做必要检查,包括: + * + *

+	 *     1、忽略多余的参数
+	 *     2、参数不够补齐默认值
+	 *     3、传入参数为null,但是目标参数类型为原始类型,做转换
+	 * 
+ * + * @param 返回对象类型 + * @param obj 对象,如果执行静态方法,此值为{@code null} + * @param method 方法(对象方法或static方法都可) + * @param args 参数对象 + * @return 结果 + * @throws InvocationTargetException 目标方法执行异常 + * @throws IllegalAccessException 访问异常 + * @since 5.8.1 + */ + @SuppressWarnings("unchecked") + public static T invokeRaw(Object obj, Method method, Object... args) throws InvocationTargetException, IllegalAccessException { + setAccessible(method); + + // 检查用户传入参数: + // 1、忽略多余的参数 + // 2、参数不够补齐默认值 + // 3、通过NullWrapperBean传递的参数,会直接赋值null + // 4、传入参数为null,但是目标参数类型为原始类型,做转换 + // 5、传入参数类型不对应,尝试转换类型 + final Class[] parameterTypes = method.getParameterTypes(); + final Object[] actualArgs = new Object[parameterTypes.length]; + if (null != args) { + for (int i = 0; i < actualArgs.length; i++) { + if (i >= args.length || null == args[i]) { + // 越界或者空值 + actualArgs[i] = ClassUtil.getDefaultValue(parameterTypes[i]); + } else if (args[i] instanceof NullWrapperBean) { + //如果是通过NullWrapperBean传递的null参数,直接赋值null + actualArgs[i] = null; + } else if (!parameterTypes[i].isAssignableFrom(args[i].getClass())) { + //对于类型不同的字段,尝试转换,转换失败则使用原对象类型 + final Object targetValue = Convert.convertWithCheck(parameterTypes[i], args[i], null, true); + if (null != targetValue) { + actualArgs[i] = targetValue; + } else { + actualArgs[i] = args[i]; + } + } else { + actualArgs[i] = args[i]; + } + } + } + + if (method.isDefault()) { + // 当方法是default方法时,尤其对象是代理对象,需使用句柄方式执行 + // 代理对象情况下调用method.invoke会导致循环引用执行,最终栈溢出 + return MethodHandleUtil.invokeSpecial(obj, method, args); + } + + return (T) method.invoke(ClassUtil.isStatic(method) ? null : obj, actualArgs); + } + + /** + * 执行对象中指定方法 + * 如果需要传递的参数为null,请使用NullWrapperBean来传递,不然会丢失类型信息 + * + * @param 返回对象类型 + * @param obj 方法所在对象 + * @param methodName 方法名 + * @param args 参数列表 + * @return 执行结果 + * @throws UtilException IllegalAccessException等异常包装 + * @see NullWrapperBean + * @since 3.1.2 + */ + public static T invoke(Object obj, String methodName, Object... args) throws UtilException { + Assert.notNull(obj, "Object to get method must be not null!"); + Assert.notBlank(methodName, "Method name must be not blank!"); + + final Method method = getMethodOfObj(obj, methodName, args); + if (null == method) { + throw new UtilException("No such method: [{}] from [{}]", methodName, obj.getClass()); + } + return invoke(obj, method, args); + } + + /** + * 设置方法为可访问(私有方法可以被外部调用) + * + * @param AccessibleObject的子类,比如Class、Method、Field等 + * @param accessibleObject 可设置访问权限的对象,比如Class、Method、Field等 + * @return 被设置可访问的对象 + * @since 4.6.8 + */ + public static T setAccessible(T accessibleObject) { + if (null != accessibleObject && !accessibleObject.isAccessible()) { + accessibleObject.setAccessible(true); + } + return accessibleObject; + } + + /** + * 设置final的field字段可以被修改 + * 只要不会被编译器内联优化的 final 属性就可以通过反射有效的进行修改 -- 修改后代码中可使用到新的值; + *

以下属性,编译器会内联优化,无法通过反射修改:

+ *
    + *
  • 基本类型 byte, char, short, int, long, float, double, boolean
  • + *
  • Literal String 类型(直接双引号字符串)
  • + *
+ *

以下属性,可以通过反射修改:

+ *
    + *
  • 基本类型的包装类 Byte、Character、Short、Long、Float、Double、Boolean
  • + *
  • 字符串,通过 new String("")实例化
  • + *
  • 自定义java类
  • + *
+ *
+	 * {@code
+	 *      //示例,移除final修饰符
+	 *      class JdbcDialects {private static final List dialects = new ArrayList<>();}
+	 *      Field field = ReflectUtil.getField(JdbcDialects.class, fieldName);
+	 * 		ReflectUtil.removeFinalModify(field);
+	 * 		ReflectUtil.setFieldValue(JdbcDialects.class, fieldName, dialects);
+	 *    }
+	 * 
+ * + * @param field 被修改的field,不可以为空 + * @throws UtilException IllegalAccessException等异常包装 + * @author dazer + * @since 5.8.8 + */ + public static void removeFinalModify(Field field) { + ModifierUtil.removeFinalModify(field); + } + + /** + * 获取方法的唯一键,结构为: + *
+	 *     返回类型#方法名:参数1类型,参数2类型...
+	 * 
+ * + * @param method 方法 + * @return 方法唯一键 + */ + private static String getUniqueKey(Method method) { + final StringBuilder sb = new StringBuilder(); + sb.append(method.getReturnType().getName()).append('#'); + sb.append(method.getName()); + Class[] parameters = method.getParameterTypes(); + for (int i = 0; i < parameters.length; i++) { + if (i == 0) { + sb.append(':'); + } else { + sb.append(','); + } + sb.append(parameters[i].getName()); + } + return sb.toString(); + } + + /** + * 获取类对应接口中的非抽象方法(default方法) + * + * @param clazz 类 + * @return 方法列表 + */ + private static List getDefaultMethodsFromInterface(Class clazz) { + List result = new ArrayList<>(); + for (Class ifc : clazz.getInterfaces()) { + for (Method m : ifc.getMethods()) { + if (!ModifierUtil.isAbstract(m)) { + result.add(m); + } + } + } + return result; + } +} diff --git a/src/main/java/cn/hutool/core/util/SerializeUtil.java b/src/main/java/cn/hutool/core/util/SerializeUtil.java new file mode 100644 index 0000000..a990b34 --- /dev/null +++ b/src/main/java/cn/hutool/core/util/SerializeUtil.java @@ -0,0 +1,67 @@ +package cn.hutool.core.util; + +import cn.hutool.core.exceptions.UtilException; +import cn.hutool.core.io.FastByteArrayOutputStream; +import cn.hutool.core.io.IoUtil; + +import java.io.ByteArrayInputStream; +import java.io.Serializable; + +/** + * 序列化工具类
+ * 注意!此工具类依赖于JDK的序列化机制,某些版本的JDK中可能存在远程注入漏洞。 + * + * @author looly + * @since 5.6.3 + */ +public class SerializeUtil { + + /** + * 序列化后拷贝流的方式克隆
+ * 对象必须实现Serializable接口 + * + * @param 对象类型 + * @param obj 被克隆对象 + * @return 克隆后的对象 + * @throws UtilException IO异常和ClassNotFoundException封装 + */ + public static T clone(T obj) { + if (!(obj instanceof Serializable)) { + return null; + } + return deserialize(serialize(obj)); + } + + /** + * 序列化
+ * 对象必须实现Serializable接口 + * + * @param 对象类型 + * @param obj 要被序列化的对象 + * @return 序列化后的字节码 + */ + public static byte[] serialize(T obj) { + if (!(obj instanceof Serializable)) { + return null; + } + final FastByteArrayOutputStream byteOut = new FastByteArrayOutputStream(); + IoUtil.writeObjects(byteOut, false, (Serializable) obj); + return byteOut.toByteArray(); + } + + /** + * 反序列化
+ * 对象必须实现Serializable接口 + * + *

+ * 注意!!! 此方法不会检查反序列化安全,可能存在反序列化漏洞风险!!! + *

+ * + * @param 对象类型 + * @param bytes 反序列化的字节码 + * @return 反序列化后的对象 + */ + public static T deserialize(byte[] bytes) { + return IoUtil.readObj(new ByteArrayInputStream(bytes)); + } +} diff --git a/src/main/java/cn/hutool/core/util/ServiceLoaderUtil.java b/src/main/java/cn/hutool/core/util/ServiceLoaderUtil.java new file mode 100644 index 0000000..d04807d --- /dev/null +++ b/src/main/java/cn/hutool/core/util/ServiceLoaderUtil.java @@ -0,0 +1,106 @@ +package cn.hutool.core.util; + +import cn.hutool.core.collection.ListUtil; + +import java.util.Iterator; +import java.util.List; +import java.util.ServiceConfigurationError; +import java.util.ServiceLoader; + +/** + * SPI机制中的服务加载工具类,流程如下 + * + *
+ *     1、创建接口,并创建实现类
+ *     2、ClassPath/META-INF/services下创建与接口全限定类名相同的文件
+ *     3、文件内容填写实现类的全限定类名
+ * 
+ * 相关介绍见:https://www.jianshu.com/p/3a3edbcd8f24 + * + * @author looly + * @since 5.1.6 + */ +public class ServiceLoaderUtil { + + /** + * 。加载第一个可用服务,如果用户定义了多个接口实现类,只获取第一个不报错的服务 + * + * @param 接口类型 + * @param clazz 服务接口 + * @return 第一个服务接口实现对象,无实现返回{@code null} + */ + public static T loadFirstAvailable(Class clazz) { + final Iterator iterator = load(clazz).iterator(); + while (iterator.hasNext()) { + try { + return iterator.next(); + } catch (ServiceConfigurationError ignore) { + // ignore + } + } + return null; + } + + /** + * 加载第一个服务,如果用户定义了多个接口实现类,只获取第一个。 + * + * @param 接口类型 + * @param clazz 服务接口 + * @return 第一个服务接口实现对象,无实现返回{@code null} + */ + public static T loadFirst(Class clazz) { + final Iterator iterator = load(clazz).iterator(); + if (iterator.hasNext()) { + return iterator.next(); + } + return null; + } + + /** + * 加载服务 + * + * @param 接口类型 + * @param clazz 服务接口 + * @return 服务接口实现列表 + */ + public static ServiceLoader load(Class clazz) { + return load(clazz, null); + } + + /** + * 加载服务 + * + * @param 接口类型 + * @param clazz 服务接口 + * @param loader {@link ClassLoader} + * @return 服务接口实现列表 + */ + public static ServiceLoader load(Class clazz, ClassLoader loader) { + return ServiceLoader.load(clazz, ObjectUtil.defaultIfNull(loader, ClassLoaderUtil::getClassLoader)); + } + + /** + * 加载服务 并已list列表返回 + * + * @param 接口类型 + * @param clazz 服务接口 + * @return 服务接口实现列表 + * @since 5.4.2 + */ + public static List loadList(Class clazz) { + return loadList(clazz, null); + } + + /** + * 加载服务 并已list列表返回 + * + * @param 接口类型 + * @param clazz 服务接口 + * @param loader {@link ClassLoader} + * @return 服务接口实现列表 + * @since 5.4.2 + */ + public static List loadList(Class clazz, ClassLoader loader) { + return ListUtil.list(false, load(clazz, loader)); + } +} diff --git a/src/main/java/cn/hutool/core/util/StrUtil.java b/src/main/java/cn/hutool/core/util/StrUtil.java new file mode 100644 index 0000000..752d0e3 --- /dev/null +++ b/src/main/java/cn/hutool/core/util/StrUtil.java @@ -0,0 +1,461 @@ +package cn.hutool.core.util; + +import cn.hutool.core.text.CharSequenceUtil; +import cn.hutool.core.text.StrBuilder; +import cn.hutool.core.text.StrFormatter; +import cn.hutool.core.text.StrPool; +import cn.hutool.core.text.TextSimilarity; + +import java.io.StringReader; +import java.io.StringWriter; +import java.nio.ByteBuffer; +import java.nio.charset.Charset; +import java.util.Map; + +/** + * 字符串工具类 + * + * @author xiaoleilu + */ +public class StrUtil extends CharSequenceUtil implements StrPool { + + // ------------------------------------------------------------------------ Blank + + /** + *

如果对象是字符串是否为空白,空白的定义如下:

+ *
    + *
  1. {@code null}
  2. + *
  3. 空字符串:{@code ""}
  4. + *
  5. 空格、全角空格、制表符、换行符,等不可见字符
  6. + *
+ * + *

例:

+ *
    + *
  • {@code StrUtil.isBlankIfStr(null) // true}
  • + *
  • {@code StrUtil.isBlankIfStr("") // true}
  • + *
  • {@code StrUtil.isBlankIfStr(" \t\n") // true}
  • + *
  • {@code StrUtil.isBlankIfStr("abc") // false}
  • + *
+ * + *

注意:该方法与 {@link #isEmptyIfStr(Object)} 的区别是: + * 该方法会校验空白字符,且性能相对于 {@link #isEmptyIfStr(Object)} 略慢。

+ * + * @param obj 对象 + * @return 如果为字符串是否为空串 + * @see StrUtil#isBlank(CharSequence) + * @since 3.3.0 + */ + public static boolean isBlankIfStr(Object obj) { + if (null == obj) { + return true; + } else if (obj instanceof CharSequence) { + return isBlank((CharSequence) obj); + } + return false; + } + // ------------------------------------------------------------------------ Empty + + /** + *

如果对象是字符串是否为空串,空的定义如下:


+ *
    + *
  1. {@code null}
  2. + *
  3. 空字符串:{@code ""}
  4. + *
+ * + *

例:

+ *
    + *
  • {@code StrUtil.isEmptyIfStr(null) // true}
  • + *
  • {@code StrUtil.isEmptyIfStr("") // true}
  • + *
  • {@code StrUtil.isEmptyIfStr(" \t\n") // false}
  • + *
  • {@code StrUtil.isEmptyIfStr("abc") // false}
  • + *
+ * + *

注意:该方法与 {@link #isBlankIfStr(Object)} 的区别是:该方法不校验空白字符。

+ * + * @param obj 对象 + * @return 如果为字符串是否为空串 + * @since 3.3.0 + */ + public static boolean isEmptyIfStr(Object obj) { + if (null == obj) { + return true; + } else if (obj instanceof CharSequence) { + return 0 == ((CharSequence) obj).length(); + } + return false; + } + + // ------------------------------------------------------------------------ Trim + + /** + * 给定字符串数组全部做去首尾空格 + * + * @param strs 字符串数组 + */ + public static void trim(String[] strs) { + if (null == strs) { + return; + } + String str; + for (int i = 0; i < strs.length; i++) { + str = strs[i]; + if (null != str) { + strs[i] = trim(str); + } + } + } + + /** + * 将对象转为字符串
+ * + *
+	 * 1、Byte数组和ByteBuffer会被转换为对应字符串的数组
+	 * 2、对象数组会调用Arrays.toString方法
+	 * 
+ * + * @param obj 对象 + * @return 字符串 + */ + public static String utf8Str(Object obj) { + return str(obj, CharsetUtil.CHARSET_UTF_8); + } + + /** + * 将对象转为字符串 + * + *
+	 * 1、Byte数组和ByteBuffer会被转换为对应字符串的数组
+	 * 2、对象数组会调用Arrays.toString方法
+	 * 
+ * + * @param obj 对象 + * @param charsetName 字符集 + * @return 字符串 + * @deprecated 请使用 {@link #str(Object, Charset)} + */ + @Deprecated + public static String str(Object obj, String charsetName) { + return str(obj, Charset.forName(charsetName)); + } + + /** + * 将对象转为字符串 + *
+	 * 	 1、Byte数组和ByteBuffer会被转换为对应字符串的数组
+	 * 	 2、对象数组会调用Arrays.toString方法
+	 * 
+ * + * @param obj 对象 + * @param charset 字符集 + * @return 字符串 + */ + public static String str(Object obj, Charset charset) { + if (null == obj) { + return null; + } + + if (obj instanceof String) { + return (String) obj; + } else if (obj instanceof byte[]) { + return str((byte[]) obj, charset); + } else if (obj instanceof Byte[]) { + return str((Byte[]) obj, charset); + } else if (obj instanceof ByteBuffer) { + return str((ByteBuffer) obj, charset); + } else if (ArrayUtil.isArray(obj)) { + return ArrayUtil.toString(obj); + } + + return obj.toString(); + } + + /** + * 将byte数组转为字符串 + * + * @param bytes byte数组 + * @param charset 字符集 + * @return 字符串 + */ + public static String str(byte[] bytes, String charset) { + return str(bytes, CharsetUtil.charset(charset)); + } + + /** + * 解码字节码 + * + * @param data 字符串 + * @param charset 字符集,如果此字段为空,则解码的结果取决于平台 + * @return 解码后的字符串 + */ + public static String str(byte[] data, Charset charset) { + if (data == null) { + return null; + } + + if (null == charset) { + return new String(data); + } + return new String(data, charset); + } + + /** + * 将Byte数组转为字符串 + * + * @param bytes byte数组 + * @param charset 字符集 + * @return 字符串 + */ + public static String str(Byte[] bytes, String charset) { + return str(bytes, CharsetUtil.charset(charset)); + } + + /** + * 解码字节码 + * + * @param data 字符串 + * @param charset 字符集,如果此字段为空,则解码的结果取决于平台 + * @return 解码后的字符串 + */ + public static String str(Byte[] data, Charset charset) { + if (data == null) { + return null; + } + + byte[] bytes = new byte[data.length]; + Byte dataByte; + for (int i = 0; i < data.length; i++) { + dataByte = data[i]; + bytes[i] = (null == dataByte) ? -1 : dataByte; + } + + return str(bytes, charset); + } + + /** + * 将编码的byteBuffer数据转换为字符串 + * + * @param data 数据 + * @param charset 字符集,如果为空使用当前系统字符集 + * @return 字符串 + */ + public static String str(ByteBuffer data, String charset) { + if (data == null) { + return null; + } + + return str(data, CharsetUtil.charset(charset)); + } + + /** + * 将编码的byteBuffer数据转换为字符串 + * + * @param data 数据 + * @param charset 字符集,如果为空使用当前系统字符集 + * @return 字符串 + */ + public static String str(ByteBuffer data, Charset charset) { + if (null == charset) { + charset = Charset.defaultCharset(); + } + return charset.decode(data).toString(); + } + + /** + * 调用对象的toString方法,null会返回“null” + * + * @param obj 对象 + * @return 字符串 + * @since 4.1.3 + * @see String#valueOf(Object) + */ + public static String toString(Object obj) { + return String.valueOf(obj); + } + + /** + * 调用对象的toString方法,null会返回{@code null} + * + * @param obj 对象 + * @return 字符串 or {@code null} + * @since 5.7.17 + */ + public static String toStringOrNull(Object obj) { + return null == obj ? null : obj.toString(); + } + + /** + * 创建StringBuilder对象 + * + * @return StringBuilder对象 + */ + public static StringBuilder builder() { + return new StringBuilder(); + } + + /** + * 创建StrBuilder对象 + * + * @return StrBuilder对象 + * @since 4.0.1 + */ + public static StrBuilder strBuilder() { + return StrBuilder.create(); + } + + /** + * 创建StringBuilder对象 + * + * @param capacity 初始大小 + * @return StringBuilder对象 + */ + public static StringBuilder builder(int capacity) { + return new StringBuilder(capacity); + } + + /** + * 创建StrBuilder对象 + * + * @param capacity 初始大小 + * @return StrBuilder对象 + * @since 4.0.1 + */ + public static StrBuilder strBuilder(int capacity) { + return StrBuilder.create(capacity); + } + + /** + * 获得StringReader + * + * @param str 字符串 + * @return StringReader + */ + public static StringReader getReader(CharSequence str) { + if (null == str) { + return null; + } + return new StringReader(str.toString()); + } + + /** + * 获得StringWriter + * + * @return StringWriter + */ + public static StringWriter getWriter() { + return new StringWriter(); + } + + /** + * 反转字符串
+ * 例如:abcd =》dcba + * + * @param str 被反转的字符串 + * @return 反转后的字符串 + * @since 3.0.9 + */ + public static String reverse(String str) { + return new String(ArrayUtil.reverse(str.toCharArray())); + } + + // ------------------------------------------------------------------------ fill + + /** + * 将已有字符串填充为规定长度,如果已有字符串超过这个长度则返回这个字符串
+ * 字符填充于字符串前 + * + * @param str 被填充的字符串 + * @param filledChar 填充的字符 + * @param len 填充长度 + * @return 填充后的字符串 + * @since 3.1.2 + */ + public static String fillBefore(String str, char filledChar, int len) { + return fill(str, filledChar, len, true); + } + + /** + * 将已有字符串填充为规定长度,如果已有字符串超过这个长度则返回这个字符串
+ * 字符填充于字符串后 + * + * @param str 被填充的字符串 + * @param filledChar 填充的字符 + * @param len 填充长度 + * @return 填充后的字符串 + * @since 3.1.2 + */ + public static String fillAfter(String str, char filledChar, int len) { + return fill(str, filledChar, len, false); + } + + /** + * 将已有字符串填充为规定长度,如果已有字符串超过这个长度则返回这个字符串 + * + * @param str 被填充的字符串 + * @param filledChar 填充的字符 + * @param len 填充长度 + * @param isPre 是否填充在前 + * @return 填充后的字符串 + * @since 3.1.2 + */ + public static String fill(String str, char filledChar, int len, boolean isPre) { + final int strLen = str.length(); + if (strLen > len) { + return str; + } + + String filledStr = StrUtil.repeat(filledChar, len - strLen); + return isPre ? filledStr.concat(str) : str.concat(filledStr); + } + + /** + * 计算两个字符串的相似度 + * + * @param str1 字符串1 + * @param str2 字符串2 + * @return 相似度 + * @since 3.2.3 + */ + public static double similar(String str1, String str2) { + return TextSimilarity.similar(str1, str2); + } + + /** + * 计算两个字符串的相似度百分比 + * + * @param str1 字符串1 + * @param str2 字符串2 + * @param scale 相似度 + * @return 相似度百分比 + * @since 3.2.3 + */ + public static String similar(String str1, String str2, int scale) { + return TextSimilarity.similar(str1, str2, scale); + } + + + /** + * 格式化文本,使用 {varName} 占位
+ * map = {a: "aValue", b: "bValue"} format("{a} and {b}", map) ---=》 aValue and bValue + * + * @param template 文本模板,被替换的部分用 {key} 表示 + * @param map 参数值对 + * @return 格式化后的文本 + */ + public static String format(CharSequence template, Map map) { + return format(template, map, true); + } + + /** + * 格式化文本,使用 {varName} 占位
+ * map = {a: "aValue", b: "bValue"} format("{a} and {b}", map) ---=》 aValue and bValue + * + * @param template 文本模板,被替换的部分用 {key} 表示 + * @param map 参数值对 + * @param ignoreNull 是否忽略 {@code null} 值,忽略则 {@code null} 值对应的变量不被替换,否则替换为"" + * @return 格式化后的文本 + * @since 5.4.3 + */ + public static String format(CharSequence template, Map map, boolean ignoreNull) { + return StrFormatter.format(template, map, ignoreNull); + } +} diff --git a/src/main/java/cn/hutool/core/util/SystemPropsUtil.java b/src/main/java/cn/hutool/core/util/SystemPropsUtil.java new file mode 100644 index 0000000..0654b97 --- /dev/null +++ b/src/main/java/cn/hutool/core/util/SystemPropsUtil.java @@ -0,0 +1,141 @@ +package cn.hutool.core.util; + +import cn.hutool.core.convert.Convert; +import cn.hutool.core.lang.Console; + +import java.util.Properties; + +/** + * 系统属性工具
+ * 此工具用于读取系统属性或环境变量信息,封装包括: + *
    + *
  • {@link System#getProperty(String)}
  • + *
  • {@link System#getenv(String)}
  • + *
+ * + * @author looly + * @since 5.7.16 + */ +public class SystemPropsUtil { + + /** Hutool自定义系统属性:是否解析日期字符串采用严格模式 */ + public static String HUTOOL_DATE_LENIENT = "hutool.date.lenient"; + + /** + * 取得系统属性,如果因为Java安全的限制而失败,则将错误打在Log中,然后返回 defaultValue + * + * @param name 属性名 + * @param defaultValue 默认值 + * @return 属性值或defaultValue + * @see System#getProperty(String) + * @see System#getenv(String) + */ + public static String get(String name, String defaultValue) { + return StrUtil.nullToDefault(get(name, false), defaultValue); + } + + /** + * 取得系统属性,如果因为Java安全的限制而失败,则将错误打在Log中,然后返回 {@code null} + * + * @param name 属性名 + * @param quiet 安静模式,不将出错信息打在{@code System.err}中 + * @return 属性值或{@code null} + * @see System#getProperty(String) + * @see System#getenv(String) + */ + public static String get(String name, boolean quiet) { + String value = null; + try { + value = System.getProperty(name); + } catch (SecurityException e) { + if (!quiet) { + Console.error("Caught a SecurityException reading the system property '{}'; " + + "the SystemUtil property value will default to null.", name); + } + } + + if (null == value) { + try { + value = System.getenv(name); + } catch (SecurityException e) { + if (!quiet) { + Console.error("Caught a SecurityException reading the system env '{}'; " + + "the SystemUtil env value will default to null.", name); + } + } + } + + return value; + } + + /** + * 获得System属性 + * + * @param key 键 + * @return 属性值 + * @see System#getProperty(String) + * @see System#getenv(String) + */ + public static String get(String key) { + return get(key, null); + } + + /** + * 获得boolean类型值 + * + * @param key 键 + * @param defaultValue 默认值 + * @return 值 + */ + public static boolean getBoolean(String key, boolean defaultValue) { + String value = get(key); + if (value == null) { + return defaultValue; + } + + return BooleanUtil.toBoolean(value); + } + + /** + * 获得int类型值 + * + * @param key 键 + * @param defaultValue 默认值 + * @return 值 + */ + public static int getInt(String key, int defaultValue) { + return Convert.toInt(get(key), defaultValue); + } + + /** + * 获得long类型值 + * + * @param key 键 + * @param defaultValue 默认值 + * @return 值 + */ + public static long getLong(String key, long defaultValue) { + return Convert.toLong(get(key), defaultValue); + } + + /** + * @return 属性列表 + */ + public static Properties getProps() { + return System.getProperties(); + } + + /** + * 设置系统属性,value为{@code null}表示移除此属性 + * + * @param key 属性名 + * @param value 属性值,{@code null}表示移除此属性 + */ + public static void set(String key, String value) { + if (null == value) { + System.clearProperty(key); + } else { + System.setProperty(key, value); + } + } +} diff --git a/src/main/java/cn/hutool/core/util/TypeUtil.java b/src/main/java/cn/hutool/core/util/TypeUtil.java new file mode 100644 index 0000000..f962b51 --- /dev/null +++ b/src/main/java/cn/hutool/core/util/TypeUtil.java @@ -0,0 +1,403 @@ +package cn.hutool.core.util; + +import cn.hutool.core.lang.ParameterizedTypeImpl; +import cn.hutool.core.lang.reflect.ActualTypeMapperPool; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.lang.reflect.TypeVariable; +import java.lang.reflect.WildcardType; +import java.util.Map; + +/** + * 针对 {@link Type} 的工具类封装
+ * 最主要功能包括: + * + *
+ * 1. 获取方法的参数和返回值类型(包括Type和Class)
+ * 2. 获取泛型参数类型(包括对象的泛型参数或集合元素的泛型类型)
+ * 
+ * + * @author Looly + * @since 3.0.8 + */ +public class TypeUtil { + + /** + * 获得Type对应的原始类 + * + * @param type {@link Type} + * @return 原始类,如果无法获取原始类,返回{@code null} + */ + public static Class getClass(Type type) { + if (null != type) { + if (type instanceof Class) { + return (Class) type; + } else if (type instanceof ParameterizedType) { + return (Class) ((ParameterizedType) type).getRawType(); + } else if (type instanceof TypeVariable) { + return (Class) ((TypeVariable) type).getBounds()[0]; + } else if (type instanceof WildcardType) { + final Type[] upperBounds = ((WildcardType) type).getUpperBounds(); + if (upperBounds.length == 1) { + return getClass(upperBounds[0]); + } + } + } + return null; + } + + /** + * 获取字段对应的Type类型
+ * 方法优先获取GenericType,获取不到则获取Type + * + * @param field 字段 + * @return {@link Type},可能为{@code null} + */ + public static Type getType(Field field) { + if (null == field) { + return null; + } + return field.getGenericType(); + } + + /** + * 获得字段的泛型类型 + * + * @param clazz Bean类 + * @param fieldName 字段名 + * @return 字段的泛型类型 + * @since 5.4.2 + */ + public static Type getFieldType(Class clazz, String fieldName) { + return getType(ReflectUtil.getField(clazz, fieldName)); + } + + /** + * 获得Field对应的原始类 + * + * @param field {@link Field} + * @return 原始类,如果无法获取原始类,返回{@code null} + * @since 3.1.2 + */ + public static Class getClass(Field field) { + return null == field ? null : field.getType(); + } + + // ----------------------------------------------------------------------------------- Param Type + + /** + * 获取方法的第一个参数类型
+ * 优先获取方法的GenericParameterTypes,如果获取不到,则获取ParameterTypes + * + * @param method 方法 + * @return {@link Type},可能为{@code null} + * @since 3.1.2 + */ + public static Type getFirstParamType(Method method) { + return getParamType(method, 0); + } + + /** + * 获取方法的第一个参数类 + * + * @param method 方法 + * @return 第一个参数类型,可能为{@code null} + * @since 3.1.2 + */ + public static Class getFirstParamClass(Method method) { + return getParamClass(method, 0); + } + + /** + * 获取方法的参数类型
+ * 优先获取方法的GenericParameterTypes,如果获取不到,则获取ParameterTypes + * + * @param method 方法 + * @param index 第几个参数的索引,从0开始计数 + * @return {@link Type},可能为{@code null} + */ + public static Type getParamType(Method method, int index) { + Type[] types = getParamTypes(method); + if (null != types && types.length > index) { + return types[index]; + } + return null; + } + + /** + * 获取方法的参数类 + * + * @param method 方法 + * @param index 第几个参数的索引,从0开始计数 + * @return 参数类,可能为{@code null} + * @since 3.1.2 + */ + public static Class getParamClass(Method method, int index) { + Class[] classes = getParamClasses(method); + if (null != classes && classes.length > index) { + return classes[index]; + } + return null; + } + + /** + * 获取方法的参数类型列表
+ * 优先获取方法的GenericParameterTypes,如果获取不到,则获取ParameterTypes + * + * @param method 方法 + * @return {@link Type}列表,可能为{@code null} + * @see Method#getGenericParameterTypes() + * @see Method#getParameterTypes() + */ + public static Type[] getParamTypes(Method method) { + return null == method ? null : method.getGenericParameterTypes(); + } + + /** + * 解析方法的参数类型列表
+ * 依赖jre\lib\rt.jar + * + * @param method t方法 + * @return 参数类型类列表 + * @see Method#getGenericParameterTypes + * @see Method#getParameterTypes + * @since 3.1.2 + */ + public static Class[] getParamClasses(Method method) { + return null == method ? null : method.getParameterTypes(); + } + + // ----------------------------------------------------------------------------------- Return Type + + /** + * 获取方法的返回值类型
+ * 获取方法的GenericReturnType + * + * @param method 方法 + * @return {@link Type},可能为{@code null} + * @see Method#getGenericReturnType() + * @see Method#getReturnType() + */ + public static Type getReturnType(Method method) { + return null == method ? null : method.getGenericReturnType(); + } + + /** + * 解析方法的返回类型类列表 + * + * @param method 方法 + * @return 返回值类型的类 + * @see Method#getGenericReturnType + * @see Method#getReturnType + * @since 3.1.2 + */ + public static Class getReturnClass(Method method) { + return null == method ? null : method.getReturnType(); + } + + // ----------------------------------------------------------------------------------- Type Argument + + /** + * 获得给定类的第一个泛型参数 + * + * @param type 被检查的类型,必须是已经确定泛型类型的类型 + * @return {@link Type},可能为{@code null} + */ + public static Type getTypeArgument(Type type) { + return getTypeArgument(type, 0); + } + + /** + * 获得给定类的泛型参数 + * + * @param type 被检查的类型,必须是已经确定泛型类型的类 + * @param index 泛型类型的索引号,即第几个泛型类型 + * @return {@link Type} + */ + public static Type getTypeArgument(Type type, int index) { + final Type[] typeArguments = getTypeArguments(type); + if (null != typeArguments && typeArguments.length > index) { + return typeArguments[index]; + } + return null; + } + + /** + * 获得指定类型中所有泛型参数类型,例如: + * + *
+	 * class A<T>
+	 * class B extends A<String>
+	 * 
+ *

+ * 通过此方法,传入B.class即可得到String + * + * @param type 指定类型 + * @return 所有泛型参数类型 + */ + public static Type[] getTypeArguments(Type type) { + if (null == type) { + return null; + } + + final ParameterizedType parameterizedType = toParameterizedType(type); + return (null == parameterizedType) ? null : parameterizedType.getActualTypeArguments(); + } + + /** + * 将{@link Type} 转换为{@link ParameterizedType}
+ * {@link ParameterizedType}用于获取当前类或父类中泛型参数化后的类型
+ * 一般用于获取泛型参数具体的参数类型,例如: + * + *

+	 * class A<T>
+	 * class B extends A<String>
+	 * 
+ *

+ * 通过此方法,传入B.class即可得到B{@link ParameterizedType},从而获取到String + * + * @param type {@link Type} + * @return {@link ParameterizedType} + * @since 4.5.2 + */ + public static ParameterizedType toParameterizedType(Type type) { + ParameterizedType result = null; + if (type instanceof ParameterizedType) { + result = (ParameterizedType) type; + } else if (type instanceof Class) { + final Class clazz = (Class) type; + Type genericSuper = clazz.getGenericSuperclass(); + if (null == genericSuper || Object.class.equals(genericSuper)) { + // 如果类没有父类,而是实现一些定义好的泛型接口,则取接口的Type + final Type[] genericInterfaces = clazz.getGenericInterfaces(); + if (ArrayUtil.isNotEmpty(genericInterfaces)) { + // 默认取第一个实现接口的泛型Type + genericSuper = genericInterfaces[0]; + } + } + result = toParameterizedType(genericSuper); + } + return result; + } + + /** + * 是否未知类型
+ * type为null或者{@link TypeVariable} 都视为未知类型 + * + * @param type Type类型 + * @return 是否未知类型 + * @since 4.5.2 + */ + public static boolean isUnknown(Type type) { + return null == type || type instanceof TypeVariable; + } + + /** + * 指定泛型数组中是否含有泛型变量 + * + * @param types 泛型数组 + * @return 是否含有泛型变量 + * @since 4.5.7 + */ + public static boolean hasTypeVariable(Type... types) { + for (Type type : types) { + if (type instanceof TypeVariable) { + return true; + } + } + return false; + } + + /** + * 获取泛型变量和泛型实际类型的对应关系Map,例如: + * + *

+	 *     T    cn.hutool.test.User
+	 *     E    java.lang.Integer
+	 * 
+ * + * @param clazz 被解析的包含泛型参数的类 + * @return 泛型对应关系Map + */ + public static Map getTypeMap(Class clazz) { + return ActualTypeMapperPool.get(clazz); + } + + /** + * 获得泛型字段对应的泛型实际类型,如果此变量没有对应的实际类型,返回null + * + * @param type 实际类型明确的类 + * @param field 字段 + * @return 实际类型,可能为Class等 + */ + public static Type getActualType(Type type, Field field) { + if (null == field) { + return null; + } + return getActualType(ObjectUtil.defaultIfNull(type, field.getDeclaringClass()), field.getGenericType()); + } + + /** + * 获得泛型变量对应的泛型实际类型,如果此变量没有对应的实际类型,返回null + * 此方法可以处理: + * + *
+	 *     1. 泛型化对象,类似于Map<User, Key<Long>>
+	 *     2. 泛型变量,类似于T
+	 * 
+ * + * @param type 类 + * @param typeVariable 泛型变量,例如T等 + * @return 实际类型,可能为Class等 + */ + public static Type getActualType(Type type, Type typeVariable) { + if (typeVariable instanceof ParameterizedType) { + return getActualType(type, (ParameterizedType) typeVariable); + } + + if (typeVariable instanceof TypeVariable) { + return ActualTypeMapperPool.getActualType(type, (TypeVariable) typeVariable); + } + + // 没有需要替换的泛型变量,原样输出 + return typeVariable; + } + + /** + * 获得泛型变量对应的泛型实际类型,如果此变量没有对应的实际类型,返回null + * 此方法可以处理复杂的泛型化对象,类似于Map<User, Key<Long>> + * + * @param type 类 + * @param parameterizedType 泛型变量,例如List<T>等 + * @return 实际类型,可能为Class等 + */ + public static Type getActualType(Type type, ParameterizedType parameterizedType) { + // 字段类型为泛型参数类型,解析对应泛型类型为真实类型,类似于List a + Type[] actualTypeArguments = parameterizedType.getActualTypeArguments(); + + // 泛型对象中含有未被转换的泛型变量 + if (TypeUtil.hasTypeVariable(actualTypeArguments)) { + actualTypeArguments = getActualTypes(type, parameterizedType.getActualTypeArguments()); + if (ArrayUtil.isNotEmpty(actualTypeArguments)) { + // 替换泛型变量为实际类型,例如List变为List + parameterizedType = new ParameterizedTypeImpl(actualTypeArguments, parameterizedType.getOwnerType(), parameterizedType.getRawType()); + } + } + + return parameterizedType; + } + + /** + * 获得泛型变量对应的泛型实际类型,如果此变量没有对应的实际类型,返回null + * + * @param type 类 + * @param typeVariables 泛型变量数组,例如T等 + * @return 实际类型数组,可能为Class等 + */ + public static Type[] getActualTypes(Type type, Type... typeVariables) { + return ActualTypeMapperPool.getActualTypes(type, typeVariables); + } +} diff --git a/src/main/java/cn/hutool/core/util/URLUtil.java b/src/main/java/cn/hutool/core/util/URLUtil.java new file mode 100644 index 0000000..fde460f --- /dev/null +++ b/src/main/java/cn/hutool/core/util/URLUtil.java @@ -0,0 +1,779 @@ +package cn.hutool.core.util; + +import cn.hutool.core.exceptions.UtilException; +import cn.hutool.core.io.FileUtil; +import cn.hutool.core.io.IORuntimeException; +import cn.hutool.core.io.IoUtil; +import cn.hutool.core.io.resource.ResourceUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.net.URLDecoder; +import cn.hutool.core.net.URLEncodeUtil; +import cn.hutool.core.net.url.UrlQuery; + +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.HttpURLConnection; +import java.net.JarURLConnection; +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.net.URLConnection; +import java.net.URLStreamHandler; +import java.nio.charset.Charset; +import java.util.Map; +import java.util.jar.JarFile; + +/** + * URL(Uniform Resource Locator)统一资源定位符相关工具类 + * + *

+ * 统一资源定位符,描述了一台特定服务器上某资源的特定位置。 + *

+ * URL组成: + *
+ *   协议://主机名[:端口]/ 路径/[:参数] [?查询]#Fragment
+ *   protocol :// hostname[:port] / path / [:parameters][?query]#fragment
+ * 
+ * + * @author xiaoleilu + */ +public class URLUtil extends URLEncodeUtil { + + /** + * 针对ClassPath路径的伪协议前缀(兼容Spring): "classpath:" + */ + public static final String CLASSPATH_URL_PREFIX = "classpath:"; + /** + * URL 前缀表示文件: "file:" + */ + public static final String FILE_URL_PREFIX = "file:"; + /** + * URL 前缀表示jar: "jar:" + */ + public static final String JAR_URL_PREFIX = "jar:"; + /** + * URL 前缀表示war: "war:" + */ + public static final String WAR_URL_PREFIX = "war:"; + /** + * URL 协议表示文件: "file" + */ + public static final String URL_PROTOCOL_FILE = "file"; + /** + * URL 协议表示Jar文件: "jar" + */ + public static final String URL_PROTOCOL_JAR = "jar"; + /** + * URL 协议表示zip文件: "zip" + */ + public static final String URL_PROTOCOL_ZIP = "zip"; + /** + * URL 协议表示WebSphere文件: "wsjar" + */ + public static final String URL_PROTOCOL_WSJAR = "wsjar"; + /** + * URL 协议表示JBoss zip文件: "vfszip" + */ + public static final String URL_PROTOCOL_VFSZIP = "vfszip"; + /** + * URL 协议表示JBoss文件: "vfsfile" + */ + public static final String URL_PROTOCOL_VFSFILE = "vfsfile"; + /** + * URL 协议表示JBoss VFS资源: "vfs" + */ + public static final String URL_PROTOCOL_VFS = "vfs"; + /** + * Jar路径以及内部文件路径的分界符: "!/" + */ + public static final String JAR_URL_SEPARATOR = "!/"; + /** + * WAR路径及内部文件路径分界符 + */ + public static final String WAR_URL_SEPARATOR = "*/"; + + /** + * 将{@link URI}转换为{@link URL} + * + * @param uri {@link URI} + * @return URL对象 + * @see URI#toURL() + * @throws UtilException {@link MalformedURLException}包装,URI格式有问题时抛出 + * @since 5.7.21 + */ + public static URL url(URI uri) throws UtilException{ + if(null == uri){ + return null; + } + try { + return uri.toURL(); + } catch (MalformedURLException e) { + throw new UtilException(e); + } + } + + /** + * 通过一个字符串形式的URL地址创建URL对象 + * + * @param url URL + * @return URL对象 + */ + public static URL url(String url) { + return url(url, null); + } + + /** + * 通过一个字符串形式的URL地址创建URL对象 + * + * @param url URL + * @param handler {@link URLStreamHandler} + * @return URL对象 + * @since 4.1.1 + */ + public static URL url(String url, URLStreamHandler handler) { + if(null == url){ + return null; + } + + // 兼容Spring的ClassPath路径 + if (url.startsWith(CLASSPATH_URL_PREFIX)) { + url = url.substring(CLASSPATH_URL_PREFIX.length()); + return ClassLoaderUtil.getClassLoader().getResource(url); + } + + try { + return new URL(null, url, handler); + } catch (MalformedURLException e) { + // 尝试文件路径 + try { + return new File(url).toURI().toURL(); + } catch (MalformedURLException ex2) { + throw new UtilException(e); + } + } + } + + /** + * 获取string协议的URL,类似于string:///xxxxx + * + * @param content 正文 + * @return URL + * @since 5.5.2 + */ + public static URI getStringURI(CharSequence content) { + if(null == content){ + return null; + } + final String contentStr = StrUtil.addPrefixIfNot(content, "string:///"); + return URI.create(contentStr); + } + + /** + * 将URL字符串转换为URL对象,并做必要验证 + * + * @param urlStr URL字符串 + * @return URL + * @since 4.1.9 + */ + public static URL toUrlForHttp(String urlStr) { + return toUrlForHttp(urlStr, null); + } + + /** + * 将URL字符串转换为URL对象,并做必要验证 + * + * @param urlStr URL字符串 + * @param handler {@link URLStreamHandler} + * @return URL + * @since 4.1.9 + */ + public static URL toUrlForHttp(String urlStr, URLStreamHandler handler) { + Assert.notBlank(urlStr, "Url is blank !"); + // 编码空白符,防止空格引起的请求异常 + urlStr = encodeBlank(urlStr); + try { + return new URL(null, urlStr, handler); + } catch (MalformedURLException e) { + throw new UtilException(e); + } + } + + /** + * 单独编码URL中的空白符,空白符编码为%20 + * + * @param urlStr URL字符串 + * @return 编码后的字符串 + * @since 4.5.14 + */ + public static String encodeBlank(CharSequence urlStr) { + if (urlStr == null) { + return null; + } + + int len = urlStr.length(); + final StringBuilder sb = new StringBuilder(len); + char c; + for (int i = 0; i < len; i++) { + c = urlStr.charAt(i); + if (CharUtil.isBlankChar(c)) { + sb.append("%20"); + } else { + sb.append(c); + } + } + return sb.toString(); + } + + /** + * 获得URL + * + * @param pathBaseClassLoader 相对路径(相对于classes) + * @return URL + * @see ResourceUtil#getResource(String) + */ + public static URL getURL(String pathBaseClassLoader) { + return ResourceUtil.getResource(pathBaseClassLoader); + } + + /** + * 获得URL + * + * @param path 相对给定 class所在的路径 + * @param clazz 指定class + * @return URL + * @see ResourceUtil#getResource(String, Class) + */ + public static URL getURL(String path, Class clazz) { + return ResourceUtil.getResource(path, clazz); + } + + /** + * 获得URL,常用于使用绝对路径时的情况 + * + * @param file URL对应的文件对象 + * @return URL + * @throws UtilException MalformedURLException + */ + public static URL getURL(File file) { + Assert.notNull(file, "File is null !"); + try { + return file.toURI().toURL(); + } catch (MalformedURLException e) { + throw new UtilException(e, "Error occured when get URL!"); + } + } + + /** + * 获得URL,常用于使用绝对路径时的情况 + * + * @param files URL对应的文件对象 + * @return URL + * @throws UtilException MalformedURLException + */ + public static URL[] getURLs(File... files) { + final URL[] urls = new URL[files.length]; + try { + for (int i = 0; i < files.length; i++) { + urls[i] = files[i].toURI().toURL(); + } + } catch (MalformedURLException e) { + throw new UtilException(e, "Error occured when get URL!"); + } + + return urls; + } + + /** + * 获取URL中域名部分,只保留URL中的协议(Protocol)、Host,其它为null。 + * + * @param url URL + * @return 域名的URI + * @since 4.6.9 + */ + public static URI getHost(URL url) { + if (null == url) { + return null; + } + + try { + return new URI(url.getProtocol(), url.getHost(), null, null); + } catch (URISyntaxException e) { + throw new UtilException(e); + } + } + + /** + * 补全相对路径 + * + * @param baseUrl 基准URL + * @param relativePath 相对URL + * @return 相对路径 + * @throws UtilException MalformedURLException + */ + public static String completeUrl(String baseUrl, String relativePath) { + baseUrl = normalize(baseUrl, false); + if (StrUtil.isBlank(baseUrl)) { + return null; + } + + try { + final URL absoluteUrl = new URL(baseUrl); + final URL parseUrl = new URL(absoluteUrl, relativePath); + return parseUrl.toString(); + } catch (MalformedURLException e) { + throw new UtilException(e); + } + } + //-------------------------------------------------------------------------- decode + + /** + * 解码URL
+ * 将%开头的16进制表示的内容解码。 + * + * @param url URL + * @return 解码后的URL + * @throws UtilException UnsupportedEncodingException + * @since 3.1.2 + */ + public static String decode(String url) throws UtilException { + return decode(url, CharsetUtil.UTF_8); + } + + /** + * 解码application/x-www-form-urlencoded字符
+ * 将%开头的16进制表示的内容解码。
+ * 规则见:https://url.spec.whatwg.org/#urlencoded-parsing + * + * @param content 被解码内容 + * @param charset 编码,null表示不解码 + * @return 编码后的字符 + * @since 4.4.1 + */ + public static String decode(String content, Charset charset) { + return URLDecoder.decode(content, charset); + } + + /** + * 解码application/x-www-form-urlencoded字符
+ * 将%开头的16进制表示的内容解码。 + * + * @param content 被解码内容 + * @param charset 编码,null表示不解码 + * @param isPlusToSpace 是否+转换为空格 + * @return 编码后的字符 + * @since 5.6.3 + */ + public static String decode(String content, Charset charset, boolean isPlusToSpace) { + return URLDecoder.decode(content, charset, isPlusToSpace); + } + + /** + * 解码application/x-www-form-urlencoded字符
+ * 将%开头的16进制表示的内容解码。 + * + * @param content URL + * @param charset 编码 + * @return 解码后的URL + * @throws UtilException UnsupportedEncodingException + */ + public static String decode(String content, String charset) throws UtilException { + return decode(content, StrUtil.isEmpty(charset) ? null : CharsetUtil.charset(charset)); + } + + /** + * 获得path部分
+ * + * @param uriStr URI路径 + * @return path + * @throws UtilException 包装URISyntaxException + */ + public static String getPath(String uriStr) { + return toURI(uriStr).getPath(); + } + + /** + * 从URL对象中获取不被编码的路径Path
+ * 对于本地路径,URL对象的getPath方法对于包含中文或空格时会被编码,导致本读路径读取错误。
+ * 此方法将URL转为URI后获取路径用于解决路径被编码的问题 + * + * @param url {@link URL} + * @return 路径 + * @since 3.0.8 + */ + public static String getDecodedPath(URL url) { + if (null == url) { + return null; + } + + String path = null; + try { + // URL对象的getPath方法对于包含中文或空格的问题 + path = toURI(url).getPath(); + } catch (UtilException e) { + // ignore + } + return (null != path) ? path : url.getPath(); + } + + /** + * 转URL为URI + * + * @param url URL + * @return URI + * @throws UtilException 包装URISyntaxException + */ + public static URI toURI(URL url) throws UtilException { + return toURI(url, false); + } + + /** + * 转URL为URI + * + * @param url URL + * @param isEncode 是否编码参数中的特殊字符(默认UTF-8编码) + * @return URI + * @throws UtilException 包装URISyntaxException + * @since 4.6.9 + */ + public static URI toURI(URL url, boolean isEncode) throws UtilException { + if (null == url) { + return null; + } + + return toURI(url.toString(), isEncode); + } + + /** + * 转字符串为URI + * + * @param location 字符串路径 + * @return URI + * @throws UtilException 包装URISyntaxException + */ + public static URI toURI(String location) throws UtilException { + return toURI(location, false); + } + + /** + * 转字符串为URI + * + * @param location 字符串路径 + * @param isEncode 是否编码参数中的特殊字符(默认UTF-8编码) + * @return URI + * @throws UtilException 包装URISyntaxException + * @since 4.6.9 + */ + public static URI toURI(String location, boolean isEncode) throws UtilException { + if (isEncode) { + location = encode(location); + } + try { + return new URI(StrUtil.trim(location)); + } catch (URISyntaxException e) { + throw new UtilException(e); + } + } + + /** + * 提供的URL是否为文件
+ * 文件协议包括"file", "vfsfile" 或 "vfs". + * + * @param url {@link URL} + * @return 是否为文件 + * @since 3.0.9 + */ + public static boolean isFileURL(URL url) { + Assert.notNull(url, "URL must be not null"); + String protocol = url.getProtocol(); + return (URL_PROTOCOL_FILE.equals(protocol) || // + URL_PROTOCOL_VFSFILE.equals(protocol) || // + URL_PROTOCOL_VFS.equals(protocol)); + } + + /** + * 提供的URL是否为jar包URL 协议包括: "jar", "zip", "vfszip" 或 "wsjar". + * + * @param url {@link URL} + * @return 是否为jar包URL + */ + public static boolean isJarURL(URL url) { + Assert.notNull(url, "URL must be not null"); + final String protocol = url.getProtocol(); + return (URL_PROTOCOL_JAR.equals(protocol) || // + URL_PROTOCOL_ZIP.equals(protocol) || // + URL_PROTOCOL_VFSZIP.equals(protocol) || // + URL_PROTOCOL_WSJAR.equals(protocol)); + } + + /** + * 提供的URL是否为Jar文件URL 判断依据为file协议且扩展名为.jar + * + * @param url the URL to check + * @return whether the URL has been identified as a JAR file URL + * @since 4.1 + */ + public static boolean isJarFileURL(URL url) { + Assert.notNull(url, "URL must be not null"); + return (URL_PROTOCOL_FILE.equals(url.getProtocol()) && // + url.getPath().toLowerCase().endsWith(FileUtil.JAR_FILE_EXT)); + } + + /** + * 从URL中获取流 + * + * @param url {@link URL} + * @return InputStream流 + * @since 3.2.1 + */ + public static InputStream getStream(URL url) { + Assert.notNull(url, "URL must be not null"); + try { + return url.openStream(); + } catch (IOException e) { + throw new IORuntimeException(e); + } + } + + /** + * 获得Reader + * + * @param url {@link URL} + * @param charset 编码 + * @return {@link BufferedReader} + * @since 3.2.1 + */ + public static BufferedReader getReader(URL url, Charset charset) { + return IoUtil.getReader(getStream(url), charset); + } + + /** + * 从URL中获取JarFile + * + * @param url URL + * @return JarFile + * @since 4.1.5 + */ + public static JarFile getJarFile(URL url) { + try { + JarURLConnection urlConnection = (JarURLConnection) url.openConnection(); + return urlConnection.getJarFile(); + } catch (IOException e) { + throw new IORuntimeException(e); + } + } + + /** + * 标准化URL字符串,包括: + * + *
    + *
  1. 自动补齐“http://”头
  2. + *
  3. 去除开头的\或者/
  4. + *
  5. 替换\为/
  6. + *
+ * + * @param url URL字符串 + * @return 标准化后的URL字符串 + */ + public static String normalize(String url) { + return normalize(url, false); + } + + /** + * 标准化URL字符串,包括: + * + *
    + *
  1. 自动补齐“http://”头
  2. + *
  3. 去除开头的\或者/
  4. + *
  5. 替换\为/
  6. + *
+ * + * @param url URL字符串 + * @param isEncodePath 是否对URL中path部分的中文和特殊字符做转义(不包括 http:, /和域名部分) + * @return 标准化后的URL字符串 + * @since 4.4.1 + */ + public static String normalize(String url, boolean isEncodePath) { + return normalize(url, isEncodePath, false); + } + + /** + * 标准化URL字符串,包括: + * + *
    + *
  1. 自动补齐“http://”头
  2. + *
  3. 去除开头的\或者/
  4. + *
  5. 替换\为/
  6. + *
  7. 如果replaceSlash为true,则替换多个/为一个
  8. + *
+ * + * @param url URL字符串 + * @param isEncodePath 是否对URL中path部分的中文和特殊字符做转义(不包括 http:, /和域名部分) + * @param replaceSlash 是否替换url body中的 // + * @return 标准化后的URL字符串 + * @since 5.5.5 + */ + public static String normalize(String url, boolean isEncodePath, boolean replaceSlash) { + if (StrUtil.isBlank(url)) { + return url; + } + final int sepIndex = url.indexOf("://"); + String protocol; + String body; + if (sepIndex > 0) { + protocol = StrUtil.subPre(url, sepIndex + 3); + body = StrUtil.subSuf(url, sepIndex + 3); + } else { + protocol = "http://"; + body = url; + } + + final int paramsSepIndex = StrUtil.indexOf(body, '?'); + String params = null; + if (paramsSepIndex > 0) { + params = StrUtil.subSuf(body, paramsSepIndex); + body = StrUtil.subPre(body, paramsSepIndex); + } + + if (StrUtil.isNotEmpty(body)) { + // 去除开头的\或者/ + //noinspection ConstantConditions + body = body.replaceAll("^[\\\\/]+", StrUtil.EMPTY); + // 替换\为/ + body = body.replace("\\", "/"); + if (replaceSlash) { + //issue#I25MZL@Gitee,双斜杠在URL中是允许存在的,默认不做替换 + body = body.replaceAll("//+", "/"); + } + } + + final int pathSepIndex = StrUtil.indexOf(body, '/'); + String domain = body; + String path = null; + if (pathSepIndex > 0) { + domain = StrUtil.subPre(body, pathSepIndex); + path = StrUtil.subSuf(body, pathSepIndex); + } + if (isEncodePath) { + path = encode(path); + } + return protocol + domain + StrUtil.nullToEmpty(path) + StrUtil.nullToEmpty(params); + } + + /** + * 将Map形式的Form表单数据转换为Url参数形式
+ * paramMap中如果key为空(null和"")会被忽略,如果value为null,会被做为空白符("")
+ * 会自动url编码键和值 + * + *
+	 * key1=v1&key2=&key3=v3
+	 * 
+ * + * @param paramMap 表单数据 + * @param charset 编码,编码为null表示不编码 + * @return url参数 + */ + public static String buildQuery(Map paramMap, Charset charset) { + return UrlQuery.of(paramMap).build(charset); + } + + /** + * 获取指定URL对应资源的内容长度,对于Http,其长度使用Content-Length头决定。 + * + * @param url URL + * @return 内容长度,未知返回-1 + * @throws IORuntimeException IO异常 + * @since 5.3.4 + */ + public static long getContentLength(URL url) throws IORuntimeException { + if (null == url) { + return -1; + } + + URLConnection conn = null; + try { + conn = url.openConnection(); + return conn.getContentLengthLong(); + } catch (IOException e) { + throw new IORuntimeException(e); + } finally { + if (conn instanceof HttpURLConnection) { + ((HttpURLConnection) conn).disconnect(); + } + } + } + + /** + * Data URI Scheme封装,数据格式为Base64。data URI scheme 允许我们使用内联(inline-code)的方式在网页中包含数据,
+ * 目的是将一些小的数据,直接嵌入到网页中,从而不用再从外部文件载入。常用于将图片嵌入网页。 + * + *

+ * Data URI的格式规范: + *

+	 *     data:[<mime type>][;charset=<charset>][;<encoding>],<encoded data>
+	 * 
+ * + * @param mimeType 可选项(null表示无),数据类型(image/png、text/plain等) + * @param data 编码后的数据 + * @return Data URI字符串 + * @since 5.3.11 + */ + public static String getDataUriBase64(String mimeType, String data) { + return getDataUri(mimeType, null, "base64", data); + } + + /** + * Data URI Scheme封装。data URI scheme 允许我们使用内联(inline-code)的方式在网页中包含数据,
+ * 目的是将一些小的数据,直接嵌入到网页中,从而不用再从外部文件载入。常用于将图片嵌入网页。 + * + *

+ * Data URI的格式规范: + *

+	 *     data:[<mime type>][;charset=<charset>][;<encoding>],<encoded data>
+	 * 
+ * + * @param mimeType 可选项(null表示无),数据类型(image/png、text/plain等) + * @param encoding 数据编码方式(US-ASCII,BASE64等) + * @param data 编码后的数据 + * @return Data URI字符串 + * @since 5.3.6 + */ + public static String getDataUri(String mimeType, String encoding, String data) { + return getDataUri(mimeType, null, encoding, data); + } + + /** + * Data URI Scheme封装。data URI scheme 允许我们使用内联(inline-code)的方式在网页中包含数据,
+ * 目的是将一些小的数据,直接嵌入到网页中,从而不用再从外部文件载入。常用于将图片嵌入网页。 + * + *

+ * Data URI的格式规范: + *

+	 *     data:[<mime type>][;charset=<charset>][;<encoding>],<encoded data>
+	 * 
+ * + * @param mimeType 可选项(null表示无),数据类型(image/png、text/plain等) + * @param charset 可选项(null表示无),源文本的字符集编码方式 + * @param encoding 数据编码方式(US-ASCII,BASE64等) + * @param data 编码后的数据 + * @return Data URI字符串 + * @since 5.3.6 + */ + public static String getDataUri(String mimeType, Charset charset, String encoding, String data) { + final StringBuilder builder = StrUtil.builder("data:"); + if (StrUtil.isNotBlank(mimeType)) { + builder.append(mimeType); + } + if (null != charset) { + builder.append(";charset=").append(charset.name()); + } + if (StrUtil.isNotBlank(encoding)) { + builder.append(';').append(encoding); + } + builder.append(',').append(data); + + return builder.toString(); + } +} diff --git a/src/main/java/cn/hutool/core/util/ZipUtil.java b/src/main/java/cn/hutool/core/util/ZipUtil.java new file mode 100644 index 0000000..828d59c --- /dev/null +++ b/src/main/java/cn/hutool/core/util/ZipUtil.java @@ -0,0 +1,1041 @@ +package cn.hutool.core.util; + +import cn.hutool.core.collection.EnumerationIter; +import cn.hutool.core.compress.Deflate; +import cn.hutool.core.compress.Gzip; +import cn.hutool.core.compress.ZipCopyVisitor; +import cn.hutool.core.compress.ZipReader; +import cn.hutool.core.compress.ZipWriter; +import cn.hutool.core.exceptions.UtilException; +import cn.hutool.core.io.FastByteArrayOutputStream; +import cn.hutool.core.io.FileUtil; +import cn.hutool.core.io.IORuntimeException; +import cn.hutool.core.io.IoUtil; +import cn.hutool.core.io.LimitedInputStream; +import cn.hutool.core.io.file.FileSystemUtil; +import cn.hutool.core.io.file.PathUtil; +import cn.hutool.core.io.resource.Resource; + +import java.io.BufferedInputStream; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileFilter; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.charset.Charset; +import java.nio.file.CopyOption; +import java.nio.file.FileAlreadyExistsException; +import java.nio.file.FileSystem; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.List; +import java.util.function.Consumer; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; +import java.util.zip.ZipInputStream; +import java.util.zip.ZipOutputStream; + +/** + * 压缩工具类 + * + * @author Looly + * @see ZipWriter + */ +public class ZipUtil { + + private static final int DEFAULT_BYTE_ARRAY_LENGTH = 32; + + /** + * 默认编码,使用平台相关编码 + */ + private static final Charset DEFAULT_CHARSET = CharsetUtil.defaultCharset(); + + /** + * 将Zip文件转换为{@link ZipFile} + * + * @param file zip文件 + * @param charset 解析zip文件的编码,null表示{@link CharsetUtil#CHARSET_UTF_8} + * @return {@link ZipFile} + */ + public static ZipFile toZipFile(File file, Charset charset) { + try { + return new ZipFile(file, ObjectUtil.defaultIfNull(charset, CharsetUtil.CHARSET_UTF_8)); + } catch (IOException e) { + throw new IORuntimeException(e); + } + } + + /** + * 获取指定{@link ZipEntry}的流,用于读取这个entry的内容
+ * 此处使用{@link LimitedInputStream} 限制最大写出大小,避免ZIP bomb漏洞 + * + * @param zipFile {@link ZipFile} + * @param zipEntry {@link ZipEntry} + * @return 流 + * @since 5.5.2 + */ + public static InputStream getStream(ZipFile zipFile, ZipEntry zipEntry) { + try { + return new LimitedInputStream(zipFile.getInputStream(zipEntry), zipEntry.getSize()); + } catch (IOException e) { + throw new IORuntimeException(e); + } + } + + /** + * 获得 {@link ZipOutputStream} + * + * @param out 压缩文件流 + * @param charset 编码 + * @return {@link ZipOutputStream} + * @since 5.8.0 + */ + public static ZipOutputStream getZipOutputStream(OutputStream out, Charset charset) { + if (out instanceof ZipOutputStream) { + return (ZipOutputStream) out; + } + return new ZipOutputStream(out, charset); + } + + /** + * 在zip文件中添加新文件或目录
+ * 新文件添加在zip根目录,文件夹包括其本身和内容
+ * 如果待添加文件夹是系统根路径(如/或c:/),则只复制文件夹下的内容 + * + * @param zipPath zip文件的Path + * @param appendFilePath 待添加文件Path(可以是文件夹) + * @param options 拷贝选项,可选是否覆盖等 + * @throws IORuntimeException IO异常 + * @since 5.7.15 + */ + public static void append(Path zipPath, Path appendFilePath, CopyOption... options) throws IORuntimeException { + try (FileSystem zipFileSystem = FileSystemUtil.createZip(zipPath.toString())) { + if (Files.isDirectory(appendFilePath)) { + Path source = appendFilePath.getParent(); + if (null == source) { + // 如果用户提供的是根路径,则不复制目录,直接复制目录下的内容 + source = appendFilePath; + } + Files.walkFileTree(appendFilePath, new ZipCopyVisitor(source, zipFileSystem, options)); + } else { + Files.copy(appendFilePath, zipFileSystem.getPath(PathUtil.getName(appendFilePath)), options); + } + } catch (FileAlreadyExistsException ignored) { + // 不覆盖情况下,文件已存在, 跳过 + } catch (IOException e) { + throw new IORuntimeException(e); + } + } + + /** + * 打包到当前目录,使用默认编码UTF-8 + * + * @param srcPath 源文件路径 + * @return 打包好的压缩文件 + * @throws UtilException IO异常 + */ + public static File zip(String srcPath) throws UtilException { + return zip(srcPath, DEFAULT_CHARSET); + } + + /** + * 打包到当前目录 + * + * @param srcPath 源文件路径 + * @param charset 编码 + * @return 打包好的压缩文件 + * @throws UtilException IO异常 + */ + public static File zip(String srcPath, Charset charset) throws UtilException { + return zip(FileUtil.file(srcPath), charset); + } + + /** + * 打包到当前目录,使用默认编码UTF-8 + * + * @param srcFile 源文件或目录 + * @return 打包好的压缩文件 + * @throws UtilException IO异常 + */ + public static File zip(File srcFile) throws UtilException { + return zip(srcFile, DEFAULT_CHARSET); + } + + /** + * 打包到当前目录 + * + * @param srcFile 源文件或目录 + * @param charset 编码 + * @return 打包好的压缩文件 + * @throws UtilException IO异常 + */ + public static File zip(File srcFile, Charset charset) throws UtilException { + final File zipFile = FileUtil.file(srcFile.getParentFile(), FileUtil.mainName(srcFile) + ".zip"); + zip(zipFile, charset, false, srcFile); + return zipFile; + } + + /** + * 对文件或文件目录进行压缩
+ * 不包含被打包目录 + * + * @param srcPath 要压缩的源文件路径。如果压缩一个文件,则为该文件的全路径;如果压缩一个目录,则为该目录的顶层目录路径 + * @param zipPath 压缩文件保存的路径,包括文件名。注意:zipPath不能是srcPath路径下的子文件夹 + * @return 压缩好的Zip文件 + * @throws UtilException IO异常 + */ + public static File zip(String srcPath, String zipPath) throws UtilException { + return zip(srcPath, zipPath, false); + } + + /** + * 对文件或文件目录进行压缩
+ * + * @param srcPath 要压缩的源文件路径。如果压缩一个文件,则为该文件的全路径;如果压缩一个目录,则为该目录的顶层目录路径 + * @param zipPath 压缩文件保存的路径,包括文件名。注意:zipPath不能是srcPath路径下的子文件夹 + * @param withSrcDir 是否包含被打包目录 + * @return 压缩文件 + * @throws UtilException IO异常 + */ + public static File zip(String srcPath, String zipPath, boolean withSrcDir) throws UtilException { + return zip(srcPath, zipPath, DEFAULT_CHARSET, withSrcDir); + } + + /** + * 对文件或文件目录进行压缩
+ * + * @param srcPath 要压缩的源文件路径。如果压缩一个文件,则为该文件的全路径;如果压缩一个目录,则为该目录的顶层目录路径 + * @param zipPath 压缩文件保存的路径,包括文件名。注意:zipPath不能是srcPath路径下的子文件夹 + * @param charset 编码 + * @param withSrcDir 是否包含被打包目录 + * @return 压缩文件 + * @throws UtilException IO异常 + */ + public static File zip(String srcPath, String zipPath, Charset charset, boolean withSrcDir) throws UtilException { + final File srcFile = FileUtil.file(srcPath); + final File zipFile = FileUtil.file(zipPath); + zip(zipFile, charset, withSrcDir, srcFile); + return zipFile; + } + + /** + * 对文件或文件目录进行压缩
+ * 使用默认UTF-8编码 + * + * @param zipFile 生成的Zip文件,包括文件名。注意:zipPath不能是srcPath路径下的子文件夹 + * @param withSrcDir 是否包含被打包目录,只针对压缩目录有效。若为false,则只压缩目录下的文件或目录,为true则将本目录也压缩 + * @param srcFiles 要压缩的源文件或目录。 + * @return 压缩文件 + * @throws UtilException IO异常 + */ + public static File zip(File zipFile, boolean withSrcDir, File... srcFiles) throws UtilException { + return zip(zipFile, DEFAULT_CHARSET, withSrcDir, srcFiles); + } + + /** + * 对文件或文件目录进行压缩 + * + * @param zipFile 生成的Zip文件,包括文件名。注意:zipPath不能是srcPath路径下的子文件夹 + * @param charset 编码 + * @param withSrcDir 是否包含被打包目录,只针对压缩目录有效。若为false,则只压缩目录下的文件或目录,为true则将本目录也压缩 + * @param srcFiles 要压缩的源文件或目录。如果压缩一个文件,则为该文件的全路径;如果压缩一个目录,则为该目录的顶层目录路径 + * @return 压缩文件 + * @throws UtilException IO异常 + */ + public static File zip(File zipFile, Charset charset, boolean withSrcDir, File... srcFiles) throws UtilException { + return zip(zipFile, charset, withSrcDir, null, srcFiles); + } + + /** + * 对文件或文件目录进行压缩 + * + * @param zipFile 生成的Zip文件,包括文件名。注意:zipPath不能是srcPath路径下的子文件夹 + * @param charset 编码 + * @param withSrcDir 是否包含被打包目录,只针对压缩目录有效。若为false,则只压缩目录下的文件或目录,为true则将本目录也压缩 + * @param filter 文件过滤器,通过实现此接口,自定义要过滤的文件(过滤掉哪些文件或文件夹不加入压缩) + * @param srcFiles 要压缩的源文件或目录。如果压缩一个文件,则为该文件的全路径;如果压缩一个目录,则为该目录的顶层目录路径 + * @return 压缩文件 + * @throws IORuntimeException IO异常 + * @since 4.6.5 + */ + public static File zip(File zipFile, Charset charset, boolean withSrcDir, FileFilter filter, File... srcFiles) throws IORuntimeException { + validateFiles(zipFile, srcFiles); + //noinspection resource + ZipWriter.of(zipFile, charset).add(withSrcDir, filter, srcFiles).close(); + return zipFile; + } + + /** + * 对文件或文件目录进行压缩 + * + * @param out 生成的Zip到的目标流,包括文件名。注意:zipPath不能是srcPath路径下的子文件夹 + * @param charset 编码 + * @param withSrcDir 是否包含被打包目录,只针对压缩目录有效。若为false,则只压缩目录下的文件或目录,为true则将本目录也压缩 + * @param filter 文件过滤器,通过实现此接口,自定义要过滤的文件(过滤掉哪些文件或文件夹不加入压缩) + * @param srcFiles 要压缩的源文件或目录。如果压缩一个文件,则为该文件的全路径;如果压缩一个目录,则为该目录的顶层目录路径 + * @throws IORuntimeException IO异常 + * @since 5.1.1 + */ + public static void zip(OutputStream out, Charset charset, boolean withSrcDir, FileFilter filter, File... srcFiles) throws IORuntimeException { + ZipWriter.of(out, charset).add(withSrcDir, filter, srcFiles).close(); + } + + /** + * 对文件或文件目录进行压缩 + * + * @param zipOutputStream 生成的Zip到的目标流,自动关闭此流 + * @param withSrcDir 是否包含被打包目录,只针对压缩目录有效。若为false,则只压缩目录下的文件或目录,为true则将本目录也压缩 + * @param filter 文件过滤器,通过实现此接口,自定义要过滤的文件(过滤掉哪些文件或文件夹不加入压缩) + * @param srcFiles 要压缩的源文件或目录。如果压缩一个文件,则为该文件的全路径;如果压缩一个目录,则为该目录的顶层目录路径 + * @throws IORuntimeException IO异常 + * @since 5.1.1 + * @deprecated 请使用 {@link #zip(OutputStream, Charset, boolean, FileFilter, File...)} + */ + @Deprecated + public static void zip(ZipOutputStream zipOutputStream, boolean withSrcDir, FileFilter filter, File... srcFiles) throws IORuntimeException { + try (final ZipWriter zipWriter = new ZipWriter(zipOutputStream)) { + zipWriter.add(withSrcDir, filter, srcFiles); + } + } + + /** + * 对流中的数据加入到压缩文件,使用默认UTF-8编码 + * + * @param zipFile 生成的Zip文件,包括文件名。注意:zipPath不能是srcPath路径下的子文件夹 + * @param path 流数据在压缩文件中的路径或文件名 + * @param data 要压缩的数据 + * @return 压缩文件 + * @throws UtilException IO异常 + * @since 3.0.6 + */ + public static File zip(File zipFile, String path, String data) throws UtilException { + return zip(zipFile, path, data, DEFAULT_CHARSET); + } + + /** + * 对流中的数据加入到压缩文件
+ * + * @param zipFile 生成的Zip文件,包括文件名。注意:zipPath不能是srcPath路径下的子文件夹 + * @param path 流数据在压缩文件中的路径或文件名 + * @param data 要压缩的数据 + * @param charset 编码 + * @return 压缩文件 + * @throws UtilException IO异常 + * @since 3.2.2 + */ + public static File zip(File zipFile, String path, String data, Charset charset) throws UtilException { + return zip(zipFile, path, IoUtil.toStream(data, charset), charset); + } + + /** + * 对流中的数据加入到压缩文件
+ * 使用默认编码UTF-8 + * + * @param zipFile 生成的Zip文件,包括文件名。注意:zipPath不能是srcPath路径下的子文件夹 + * @param path 流数据在压缩文件中的路径或文件名 + * @param in 要压缩的源 + * @return 压缩文件 + * @throws UtilException IO异常 + * @since 3.0.6 + */ + public static File zip(File zipFile, String path, InputStream in) throws UtilException { + return zip(zipFile, path, in, DEFAULT_CHARSET); + } + + /** + * 对流中的数据加入到压缩文件 + * + * @param zipFile 生成的Zip文件,包括文件名。注意:zipPath不能是srcPath路径下的子文件夹 + * @param path 流数据在压缩文件中的路径或文件名 + * @param in 要压缩的源,默认关闭 + * @param charset 编码 + * @return 压缩文件 + * @throws UtilException IO异常 + * @since 3.2.2 + */ + public static File zip(File zipFile, String path, InputStream in, Charset charset) throws UtilException { + return zip(zipFile, new String[]{path}, new InputStream[]{in}, charset); + } + + /** + * 对流中的数据加入到压缩文件
+ * 路径列表和流列表长度必须一致 + * + * @param zipFile 生成的Zip文件,包括文件名。注意:zipPath不能是srcPath路径下的子文件夹 + * @param paths 流数据在压缩文件中的路径或文件名 + * @param ins 要压缩的源,添加完成后自动关闭流 + * @return 压缩文件 + * @throws UtilException IO异常 + * @since 3.0.9 + */ + public static File zip(File zipFile, String[] paths, InputStream[] ins) throws UtilException { + return zip(zipFile, paths, ins, DEFAULT_CHARSET); + } + + /** + * 对流中的数据加入到压缩文件
+ * 路径列表和流列表长度必须一致 + * + * @param zipFile 生成的Zip文件,包括文件名。注意:zipPath不能是srcPath路径下的子文件夹 + * @param paths 流数据在压缩文件中的路径或文件名 + * @param ins 要压缩的源,添加完成后自动关闭流 + * @param charset 编码 + * @return 压缩文件 + * @throws UtilException IO异常 + * @since 3.0.9 + */ + public static File zip(File zipFile, String[] paths, InputStream[] ins, Charset charset) throws UtilException { + try (final ZipWriter zipWriter = ZipWriter.of(zipFile, charset)) { + zipWriter.add(paths, ins); + } + + return zipFile; + } + + /** + * 将文件流压缩到目标流中 + * + * @param out 目标流,压缩完成自动关闭 + * @param paths 流数据在压缩文件中的路径或文件名 + * @param ins 要压缩的源,添加完成后自动关闭流 + * @since 5.5.2 + */ + public static void zip(OutputStream out, String[] paths, InputStream[] ins) { + zip(getZipOutputStream(out, DEFAULT_CHARSET), paths, ins); + } + + /** + * 将文件流压缩到目标流中 + * + * @param zipOutputStream 目标流,压缩完成自动关闭 + * @param paths 流数据在压缩文件中的路径或文件名 + * @param ins 要压缩的源,添加完成后自动关闭流 + * @throws IORuntimeException IO异常 + * @since 5.5.2 + */ + public static void zip(ZipOutputStream zipOutputStream, String[] paths, InputStream[] ins) throws IORuntimeException { + try (final ZipWriter zipWriter = new ZipWriter(zipOutputStream)) { + zipWriter.add(paths, ins); + } + } + + /** + * 对流中的数据加入到压缩文件
+ * 路径列表和流列表长度必须一致 + * + * @param zipFile 生成的Zip文件,包括文件名。注意:zipPath不能是srcPath路径下的子文件夹 + * @param charset 编码 + * @param resources 需要压缩的资源,资源的路径为{@link Resource#getName()} + * @return 压缩文件 + * @throws UtilException IO异常 + * @since 5.5.2 + */ + public static File zip(File zipFile, Charset charset, Resource... resources) throws UtilException { + //noinspection resource + ZipWriter.of(zipFile, charset).add(resources).close(); + return zipFile; + } + + // ---------------------------------------------------------------------------------------------- Unzip + + /** + * 解压到文件名相同的目录中,默认编码UTF-8 + * + * @param zipFilePath 压缩文件路径 + * @return 解压的目录 + * @throws UtilException IO异常 + */ + public static File unzip(String zipFilePath) throws UtilException { + return unzip(zipFilePath, DEFAULT_CHARSET); + } + + /** + * 解压到文件名相同的目录中 + * + * @param zipFilePath 压缩文件路径 + * @param charset 编码 + * @return 解压的目录 + * @throws UtilException IO异常 + * @since 3.2.2 + */ + public static File unzip(String zipFilePath, Charset charset) throws UtilException { + return unzip(FileUtil.file(zipFilePath), charset); + } + + /** + * 解压到文件名相同的目录中,使用UTF-8编码 + * + * @param zipFile 压缩文件 + * @return 解压的目录 + * @throws UtilException IO异常 + * @since 3.2.2 + */ + public static File unzip(File zipFile) throws UtilException { + return unzip(zipFile, DEFAULT_CHARSET); + } + + /** + * 解压到文件名相同的目录中 + * + * @param zipFile 压缩文件 + * @param charset 编码 + * @return 解压的目录 + * @throws UtilException IO异常 + * @since 3.2.2 + */ + public static File unzip(File zipFile, Charset charset) throws UtilException { + final File destDir = FileUtil.file(zipFile.getParentFile(), FileUtil.mainName(zipFile)); + return unzip(zipFile, destDir, charset); + } + + /** + * 解压,默认UTF-8编码 + * + * @param zipFilePath 压缩文件的路径 + * @param outFileDir 解压到的目录 + * @return 解压的目录 + * @throws UtilException IO异常 + */ + public static File unzip(String zipFilePath, String outFileDir) throws UtilException { + return unzip(zipFilePath, outFileDir, DEFAULT_CHARSET); + } + + /** + * 解压 + * + * @param zipFilePath 压缩文件的路径 + * @param outFileDir 解压到的目录 + * @param charset 编码 + * @return 解压的目录 + * @throws UtilException IO异常 + */ + public static File unzip(String zipFilePath, String outFileDir, Charset charset) throws UtilException { + return unzip(FileUtil.file(zipFilePath), FileUtil.mkdir(outFileDir), charset); + } + + /** + * 解压,默认使用UTF-8编码 + * + * @param zipFile zip文件 + * @param outFile 解压到的目录 + * @return 解压的目录 + * @throws UtilException IO异常 + */ + public static File unzip(File zipFile, File outFile) throws UtilException { + return unzip(zipFile, outFile, DEFAULT_CHARSET); + } + + /** + * 解压 + * + * @param zipFile zip文件 + * @param outFile 解压到的目录 + * @param charset 编码 + * @return 解压的目录 + * @since 3.2.2 + */ + public static File unzip(File zipFile, File outFile, Charset charset) { + return unzip(toZipFile(zipFile, charset), outFile); + } + + /** + * 解压 + * + * @param zipFile zip文件,附带编码信息,使用完毕自动关闭 + * @param outFile 解压到的目录 + * @return 解压的目录 + * @throws IORuntimeException IO异常 + * @since 4.5.8 + */ + public static File unzip(ZipFile zipFile, File outFile) throws IORuntimeException { + return unzip(zipFile, outFile, -1); + } + + /** + * 限制解压后文件大小 + * + * @param zipFile zip文件,附带编码信息,使用完毕自动关闭 + * @param outFile 解压到的目录 + * @param limit 限制解压文件大小(单位B) + * @return 解压的目录 + * @throws IORuntimeException IO异常 + * @since 5.8.5 + */ + public static File unzip(ZipFile zipFile, File outFile, long limit) throws IORuntimeException { + if (outFile.exists() && outFile.isFile()) { + throw new IllegalArgumentException( + StrUtil.format("Target path [{}] exist!", outFile.getAbsolutePath())); + } + + // pr#726@Gitee + if (limit > 0) { + final Enumeration zipEntries = zipFile.entries(); + long zipFileSize = 0L; + while (zipEntries.hasMoreElements()) { + final ZipEntry zipEntry = zipEntries.nextElement(); + zipFileSize += zipEntry.getSize(); + if (zipFileSize > limit) { + throw new IllegalArgumentException("The file size exceeds the limit"); + } + } + } + + try (final ZipReader reader = new ZipReader(zipFile)) { + reader.readTo(outFile); + } + return outFile; + } + + /** + * 获取压缩包中的指定文件流 + * + * @param zipFile 压缩文件 + * @param charset 编码 + * @param path 需要提取文件的文件名或路径 + * @return 压缩文件流,如果未找到返回{@code null} + * @since 5.5.2 + */ + public static InputStream get(File zipFile, Charset charset, String path) { + return get(toZipFile(zipFile, charset), path); + } + + /** + * 获取压缩包中的指定文件流 + * + * @param zipFile 压缩文件 + * @param path 需要提取文件的文件名或路径 + * @return 压缩文件流,如果未找到返回{@code null} + * @since 5.5.2 + */ + public static InputStream get(ZipFile zipFile, String path) { + final ZipEntry entry = zipFile.getEntry(path); + if (null != entry) { + return getStream(zipFile, entry); + } + return null; + } + + /** + * 读取并处理Zip文件中的每一个{@link ZipEntry} + * + * @param zipFile Zip文件 + * @param consumer {@link ZipEntry}处理器 + * @since 5.5.2 + */ + public static void read(ZipFile zipFile, Consumer consumer) { + try (final ZipReader reader = new ZipReader(zipFile)) { + reader.read(consumer); + } + } + + /** + * 解压
+ * ZIP条目不使用高速缓冲。 + * + * @param in zip文件流,使用完毕自动关闭 + * @param outFile 解压到的目录 + * @param charset 编码 + * @return 解压的目录 + * @throws UtilException IO异常 + * @since 4.5.8 + */ + public static File unzip(InputStream in, File outFile, Charset charset) throws UtilException { + if (null == charset) { + charset = DEFAULT_CHARSET; + } + return unzip(new ZipInputStream(in, charset), outFile); + } + + /** + * 解压
+ * ZIP条目不使用高速缓冲。 + * + * @param zipStream zip文件流,包含编码信息 + * @param outFile 解压到的目录 + * @return 解压的目录 + * @throws UtilException IO异常 + * @since 4.5.8 + */ + public static File unzip(ZipInputStream zipStream, File outFile) throws UtilException { + try (final ZipReader reader = new ZipReader(zipStream)) { + reader.readTo(outFile); + } + return outFile; + } + + /** + * 读取并处理Zip流中的每一个{@link ZipEntry} + * + * @param zipStream zip文件流,包含编码信息 + * @param consumer {@link ZipEntry}处理器 + * @since 5.5.2 + */ + public static void read(ZipInputStream zipStream, Consumer consumer) { + try (final ZipReader reader = new ZipReader(zipStream)) { + reader.read(consumer); + } + } + + /** + * 从Zip文件中提取指定的文件为bytes + * + * @param zipFilePath Zip文件 + * @param name 文件名,如果存在于子文件夹中,此文件名必须包含目录名,例如images/aaa.txt + * @return 文件内容bytes + * @since 4.1.8 + */ + public static byte[] unzipFileBytes(String zipFilePath, String name) { + return unzipFileBytes(zipFilePath, DEFAULT_CHARSET, name); + } + + /** + * 从Zip文件中提取指定的文件为bytes + * + * @param zipFilePath Zip文件 + * @param charset 编码 + * @param name 文件名,如果存在于子文件夹中,此文件名必须包含目录名,例如images/aaa.txt + * @return 文件内容bytes + * @since 4.1.8 + */ + public static byte[] unzipFileBytes(String zipFilePath, Charset charset, String name) { + return unzipFileBytes(FileUtil.file(zipFilePath), charset, name); + } + + /** + * 从Zip文件中提取指定的文件为bytes + * + * @param zipFile Zip文件 + * @param name 文件名,如果存在于子文件夹中,此文件名必须包含目录名,例如images/aaa.txt + * @return 文件内容bytes + * @since 4.1.8 + */ + public static byte[] unzipFileBytes(File zipFile, String name) { + return unzipFileBytes(zipFile, DEFAULT_CHARSET, name); + } + + /** + * 从Zip文件中提取指定的文件为bytes + * + * @param zipFile Zip文件 + * @param charset 编码 + * @param name 文件名,如果存在于子文件夹中,此文件名必须包含目录名,例如images/aaa.txt + * @return 文件内容bytes + * @since 4.1.8 + */ + public static byte[] unzipFileBytes(File zipFile, Charset charset, String name) { + try (final ZipReader reader = ZipReader.of(zipFile, charset)) { + return IoUtil.readBytes(reader.get(name)); + } + } + + // ----------------------------------------------------------------------------- Gzip + + /** + * Gzip压缩处理 + * + * @param content 被压缩的字符串 + * @param charset 编码 + * @return 压缩后的字节流 + * @throws UtilException IO异常 + */ + public static byte[] gzip(String content, String charset) throws UtilException { + return gzip(StrUtil.bytes(content, charset)); + } + + /** + * Gzip压缩处理 + * + * @param buf 被压缩的字节流 + * @return 压缩后的字节流 + * @throws UtilException IO异常 + */ + public static byte[] gzip(byte[] buf) throws UtilException { + return gzip(new ByteArrayInputStream(buf), buf.length); + } + + /** + * Gzip压缩文件 + * + * @param file 被压缩的文件 + * @return 压缩后的字节流 + * @throws UtilException IO异常 + */ + public static byte[] gzip(File file) throws UtilException { + BufferedInputStream in = null; + try { + in = FileUtil.getInputStream(file); + return gzip(in, (int) file.length()); + } finally { + IoUtil.close(in); + } + } + + /** + * Gzip压缩文件 + * + * @param in 被压缩的流 + * @return 压缩后的字节流 + * @throws UtilException IO异常 + * @since 4.1.18 + */ + public static byte[] gzip(InputStream in) throws UtilException { + return gzip(in, DEFAULT_BYTE_ARRAY_LENGTH); + } + + /** + * Gzip压缩文件 + * + * @param in 被压缩的流 + * @param length 预估长度 + * @return 压缩后的字节流 + * @throws UtilException IO异常 + * @since 4.1.18 + */ + public static byte[] gzip(InputStream in, int length) throws UtilException { + final ByteArrayOutputStream bos = new ByteArrayOutputStream(length); + Gzip.of(in, bos).gzip().close(); + return bos.toByteArray(); + } + + /** + * Gzip解压缩处理 + * + * @param buf 压缩过的字节流 + * @param charset 编码 + * @return 解压后的字符串 + * @throws UtilException IO异常 + */ + public static String unGzip(byte[] buf, String charset) throws UtilException { + return StrUtil.str(unGzip(buf), charset); + } + + /** + * Gzip解压处理 + * + * @param buf buf + * @return bytes + * @throws UtilException IO异常 + */ + public static byte[] unGzip(byte[] buf) throws UtilException { + return unGzip(new ByteArrayInputStream(buf), buf.length); + } + + /** + * Gzip解压处理 + * + * @param in Gzip数据 + * @return 解压后的数据 + * @throws UtilException IO异常 + */ + public static byte[] unGzip(InputStream in) throws UtilException { + return unGzip(in, DEFAULT_BYTE_ARRAY_LENGTH); + } + + /** + * Gzip解压处理 + * + * @param in Gzip数据 + * @param length 估算长度,如果无法确定请传入{@link #DEFAULT_BYTE_ARRAY_LENGTH} + * @return 解压后的数据 + * @throws UtilException IO异常 + * @since 4.1.18 + */ + public static byte[] unGzip(InputStream in, int length) throws UtilException { + FastByteArrayOutputStream bos = new FastByteArrayOutputStream(length); + Gzip.of(in, bos).unGzip().close(); + return bos.toByteArray(); + } + + // ----------------------------------------------------------------------------- Zlib + + /** + * Zlib压缩处理 + * + * @param content 被压缩的字符串 + * @param charset 编码 + * @param level 压缩级别,1~9 + * @return 压缩后的字节流 + * @since 4.1.4 + */ + public static byte[] zlib(String content, String charset, int level) { + return zlib(StrUtil.bytes(content, charset), level); + } + + /** + * Zlib压缩文件 + * + * @param file 被压缩的文件 + * @param level 压缩级别 + * @return 压缩后的字节流 + * @since 4.1.4 + */ + public static byte[] zlib(File file, int level) { + BufferedInputStream in = null; + try { + in = FileUtil.getInputStream(file); + return zlib(in, level, (int) file.length()); + } finally { + IoUtil.close(in); + } + } + + /** + * 打成Zlib压缩包 + * + * @param buf 数据 + * @param level 压缩级别,0~9 + * @return 压缩后的bytes + * @since 4.1.4 + */ + public static byte[] zlib(byte[] buf, int level) { + return zlib(new ByteArrayInputStream(buf), level, buf.length); + } + + /** + * 打成Zlib压缩包 + * + * @param in 数据流 + * @param level 压缩级别,0~9 + * @return 压缩后的bytes + * @since 4.1.19 + */ + public static byte[] zlib(InputStream in, int level) { + return zlib(in, level, DEFAULT_BYTE_ARRAY_LENGTH); + } + + /** + * 打成Zlib压缩包 + * + * @param in 数据流 + * @param level 压缩级别,0~9 + * @param length 预估大小 + * @return 压缩后的bytes + * @since 4.1.19 + */ + public static byte[] zlib(InputStream in, int level, int length) { + final ByteArrayOutputStream out = new ByteArrayOutputStream(length); + Deflate.of(in, out, false).deflater(level); + return out.toByteArray(); + } + + /** + * Zlib解压缩处理 + * + * @param buf 压缩过的字节流 + * @param charset 编码 + * @return 解压后的字符串 + * @since 4.1.4 + */ + public static String unZlib(byte[] buf, String charset) { + return StrUtil.str(unZlib(buf), charset); + } + + /** + * 解压缩zlib + * + * @param buf 数据 + * @return 解压后的bytes + * @since 4.1.4 + */ + public static byte[] unZlib(byte[] buf) { + return unZlib(new ByteArrayInputStream(buf), buf.length); + } + + /** + * 解压缩zlib + * + * @param in 数据流 + * @return 解压后的bytes + * @since 4.1.19 + */ + public static byte[] unZlib(InputStream in) { + return unZlib(in, DEFAULT_BYTE_ARRAY_LENGTH); + } + + /** + * 解压缩zlib + * + * @param in 数据流 + * @param length 预估长度 + * @return 解压后的bytes + * @since 4.1.19 + */ + public static byte[] unZlib(InputStream in, int length) { + final ByteArrayOutputStream out = new ByteArrayOutputStream(length); + Deflate.of(in, out, false).inflater(); + return out.toByteArray(); + } + + /** + * 获取Zip文件中指定目录下的所有文件,只显示文件,不显示目录
+ * 此方法并不会关闭{@link ZipFile}。 + * + * @param zipFile Zip文件 + * @param dir 目录前缀(目录前缀不包含开头的/) + * @return 文件列表 + * @since 4.6.6 + */ + public static List listFileNames(ZipFile zipFile, String dir) { + if (StrUtil.isNotBlank(dir)) { + // 目录尾部添加"/" + dir = StrUtil.addSuffixIfNot(dir, StrUtil.SLASH); + } + + final List fileNames = new ArrayList<>(); + String name; + for (ZipEntry entry : new EnumerationIter<>(zipFile.entries())) { + name = entry.getName(); + if (StrUtil.isEmpty(dir) || name.startsWith(dir)) { + final String nameSuffix = StrUtil.removePrefix(name, dir); + if (StrUtil.isNotEmpty(nameSuffix) && !StrUtil.contains(nameSuffix, CharUtil.SLASH)) { + fileNames.add(nameSuffix); + } + } + } + + return fileNames; + } + + // ---------------------------------------------------------------------------------------------- Private method start + + /** + * 判断压缩文件保存的路径是否为源文件路径的子文件夹,如果是,则抛出异常(防止无限递归压缩的发生) + * + * @param zipFile 压缩后的产生的文件路径 + * @param srcFiles 被压缩的文件或目录 + */ + private static void validateFiles(File zipFile, File... srcFiles) throws UtilException { + if (zipFile.isDirectory()) { + throw new UtilException("Zip file [{}] must not be a directory !", zipFile.getAbsoluteFile()); + } + + for (File srcFile : srcFiles) { + if (null == srcFile) { + continue; + } + if (!srcFile.exists()) { + throw new UtilException(StrUtil.format("File [{}] not exist!", srcFile.getAbsolutePath())); + } + + // issue#1961@Github + // 当 zipFile = new File("temp.zip") 时, zipFile.getParentFile() == null + File parentFile; + try { + parentFile = zipFile.getCanonicalFile().getParentFile(); + } catch (IOException e) { + parentFile = zipFile.getParentFile(); + } + + // 压缩文件不能位于被压缩的目录内 + if (srcFile.isDirectory() && FileUtil.isSub(srcFile, parentFile)) { + throw new UtilException("Zip file path [{}] must not be the child directory of [{}] !", zipFile.getPath(), srcFile.getPath()); + } + } + } + // ---------------------------------------------------------------------------------------------- Private method end + +} diff --git a/src/main/java/cn/hutool/core/util/package-info.java b/src/main/java/cn/hutool/core/util/package-info.java new file mode 100644 index 0000000..0232912 --- /dev/null +++ b/src/main/java/cn/hutool/core/util/package-info.java @@ -0,0 +1,7 @@ +/** + * 提供各种工具方法,按照归类入口为XXXUtil,如字符串工具StrUtil等 + * + * @author looly + * + */ +package cn.hutool.core.util; \ No newline at end of file diff --git a/src/main/java/com/keyware/regtool/AESUtil.java b/src/main/java/com/keyware/regtool/AESUtil.java new file mode 100644 index 0000000..b64ba64 --- /dev/null +++ b/src/main/java/com/keyware/regtool/AESUtil.java @@ -0,0 +1,220 @@ +package com.keyware.regtool; + +import javax.crypto.Cipher; +import javax.crypto.spec.SecretKeySpec; +import java.io.*; + +/** + * AES 加密解密工具类 + * + * @author GuoXin + * @date 2023/11/3 + */ +public class AESUtil { + private static final String defaultCharset = "utf-8"; + /** + * @param str 要加密的字符串 + * @param key 加密的密钥 + * @return 加密后的字节码 + * @throws Exception + */ + private static byte[] aesEncrypt(String str, String key, String charset) throws Exception { + if (str == null || key == null) { + return null; + } + if(charset == null){ + charset = defaultCharset; + } + key = String.format("%-16.16s", key); + Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding"); + cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(key.getBytes(charset), "AES")); + return cipher.doFinal(str.getBytes(charset)); + } + + public static String aesDecrypt(byte[] bytes, String key, String charset) throws Exception { + if (bytes == null || key == null) { + return null; + } + if(charset == null){ + charset = defaultCharset; + } + key = String.format("%-16.16s", key); + Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding"); + cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(key.getBytes(charset), "AES")); + bytes = cipher.doFinal(bytes); + return new String(bytes, charset); + } + + public static void fileAesEncrypt(String srcFile, String newFile, String password) { + File file = new File(srcFile); + File file1 = new File(newFile); + BufferedReader reader = null; + try { + FileWriter fileWriter = new FileWriter(file1); + reader = new BufferedReader(new FileReader(file)); + String tempString; + StringBuilder content = new StringBuilder(); + while ((tempString = reader.readLine()) != null) { + content.append(tempString); + } + byte[] contentByte = aesEncrypt(String.valueOf(content), password, null); + String encryptResultStr = parseByte2HexStr(contentByte); + fileWriter.write(encryptResultStr); + reader.close(); + fileWriter.close(); + } catch (Exception e) { + e.printStackTrace(); + } finally { + if (reader != null) { + try { + reader.close(); + } catch (IOException ignored) { + } + } + } + } + + public static String fileAesDecrypt(String srcFile, String password) { + File file = new File(srcFile); + try (BufferedReader reader = new BufferedReader(new FileReader(file))) { + String tempString; + StringBuilder content = new StringBuilder(); + while ((tempString = reader.readLine()) != null) { + content.append(tempString); + } + byte[] decryptFrom = parseHexStr2Byte(String.valueOf(content)); + reader.close(); + return (aesDecrypt(decryptFrom, password, null)); + } catch (Exception e) { + e.printStackTrace(); + } + return null; + } + + public static String stringAesEncrypt(String content, String password) { + byte[] contentByte = new byte[0]; + try { + contentByte = aesEncrypt(content, password, null); + } catch (Exception e) { + e.printStackTrace(); + } + return parseByte2HexStr(contentByte); + } + + public static String stringAesDecrypt(String aesText, String password) { + try { + return aesDecrypt(parseHexStr2Byte(aesText), password, null); + } catch (Exception e) { + e.printStackTrace(); + return null; + } + } + + + public static byte[] parseHexStr2Byte(String hexStr) { + if (hexStr.length() < 1) { + return null; + } + try { + byte[] result = new byte[hexStr.length() / 2]; + for (int i = 0; i < hexStr.length() / 2; i++) { + int high = Integer.parseInt(hexStr.substring(i * 2, i * 2 + 1), 16); + int low = Integer.parseInt(hexStr.substring(i * 2 + 1, i * 2 + 2), 16); + result[i] = (byte) (high * 16 + low); + } + return result; + } catch (Exception e) { + // TODO: handle exception + e.printStackTrace(); + } + + return null; + } + + public static String parseByte2HexStr(byte[] buf) { + StringBuffer sb = new StringBuffer(); + for (int i = 0; i < buf.length; i++) { + String hex = Integer.toHexString(buf[i] & 0xFF); + if (hex.length() == 1) { + hex = '0' + hex; + } + sb.append(hex.toUpperCase()); + } + return sb.toString(); + } + + + public static void main(String[] args) { + try { + String text = "", key = ""; + boolean de = false; + for (String arg : args) { + String[] ps = arg.split("="); + if (ps.length == 2) { + switch (ps[0]) { + case "-text": + text = ps[1]; + break; + case "-key": + key = ps[1]; + break; + case "-d": + de = true; + } + } + } + if ("".equals(text)) { + System.out.println("error: -text not set"); + System.exit(1); + } + if ("".equals(key)) { + System.out.println("error: -key not set"); + System.exit(1); + } + + if (de) { + System.out.println("加密成功:" + stringAesEncrypt(text, key)); + } else { + System.out.println("解密成功:" + stringAesDecrypt(text, key)); + } + + } catch (Exception e) { + e.printStackTrace(); + } + + + } + + public static String Make_CRC(byte[] data) { + byte[] buf = new byte[data.length];// 存储需要产生校验码的数据 + System.arraycopy(data, 0, buf, 0, data.length); + int len = buf.length; + int crc = 0xFFFF;//16位 + for (int pos = 0; pos < len; pos++) { + if (buf[pos] < 0) { + crc ^= buf[pos] + 256; // XOR byte into least sig. byte of + // crc + } else { + crc ^= buf[pos]; // XOR byte into least sig. byte of crc + } + for (int i = 8; i != 0; i--) { // Loop over each bit + if ((crc & 0x0001) != 0) { // If the LSB is set + crc >>= 1; // Shift right and XOR 0xA001 + crc ^= 0xA001; + } else + // Else LSB is not set + crc >>= 1; // Just shift right + } + } + String c = Integer.toHexString(crc); + if (c.length() == 4) { + c = c.substring(2, 4) + c.substring(0, 2); + } else if (c.length() == 3) { + c = "0" + c; + c = c.substring(2, 4) + c.substring(0, 2); + } else if (c.length() == 2) { + c = "0" + c.charAt(1) + "0" + c.charAt(0); + } + return c; + } +} diff --git a/src/main/java/com/keyware/regtool/Application.java b/src/main/java/com/keyware/regtool/Application.java new file mode 100644 index 0000000..6373334 --- /dev/null +++ b/src/main/java/com/keyware/regtool/Application.java @@ -0,0 +1,49 @@ +package com.keyware.regtool; + +import javafx.beans.value.ObservableValue; +import javafx.concurrent.Worker; +import javafx.scene.Scene; +import javafx.scene.image.Image; +import javafx.scene.layout.BorderPane; +import javafx.scene.web.WebEngine; +import javafx.scene.web.WebView; +import javafx.stage.Stage; + +import java.io.IOException; +import java.util.Objects; +import netscape.javascript.JSObject; + +public class Application extends javafx.application.Application { + @Override + public void start(Stage stage) throws IOException { + stage.setTitle("EagleEye 注册码生成器"); + stage.setFullScreen(false); + stage.setResizable(false); + stage.getIcons().add(new Image("/logo.png")); + + BorderPane pane = new BorderPane(); + WebView webView = new WebView(); + WebEngine engine = webView.getEngine(); + WebViewController controller = new WebViewController(); + + engine.getLoadWorker().stateProperty().addListener((ObservableValue ov, Worker.State oldState, Worker.State newState) -> { + if (newState == Worker.State.SUCCEEDED) { + // 获取JS的window对象 + JSObject window = (JSObject) engine.executeScript("window"); + // 讲controller注入到window对象中 + window.setMember("controller", controller); + + } + }); + + webView.getEngine().load(Objects.requireNonNull(getClass().getResource("/template/index.html")).toExternalForm()); + pane.setCenter(webView); + Scene scene = new Scene(pane, 680, 280); + stage.setScene(scene); + stage.show(); + } + + public static void main(String[] args) { + launch(); + } +} \ No newline at end of file diff --git a/src/main/java/com/keyware/regtool/Main.java b/src/main/java/com/keyware/regtool/Main.java new file mode 100644 index 0000000..c4ba75a --- /dev/null +++ b/src/main/java/com/keyware/regtool/Main.java @@ -0,0 +1,13 @@ +package com.keyware.regtool; + +/** + * TODO Main + * + * @author GuoXin + * @date 2023/11/7 + */ +public class Main { + public static void main(String[] args) { + Application.main(args); + } +} diff --git a/src/main/java/com/keyware/regtool/RegInfo.java b/src/main/java/com/keyware/regtool/RegInfo.java new file mode 100644 index 0000000..4ea1432 --- /dev/null +++ b/src/main/java/com/keyware/regtool/RegInfo.java @@ -0,0 +1,49 @@ +package com.keyware.regtool; + +/** + * 注册信息 + * + * @author GuoXin + * @date 2023/11/6 + */ +public class RegInfo { + // 用户码 + private String userCode; + // 过期时间 + private Long expTime; + // 生成时间 + private Long genTime; + + public RegInfo(String userCode) { + this.userCode = userCode; + } + + public String getUserCode() { + return userCode; + } + + public void setUserCode(String userCode) { + this.userCode = userCode; + } + + public Long getExpTime() { + return expTime; + } + + public void setExpTime(Long expTime) { + this.expTime = expTime; + } + + public Long getGenTime() { + return genTime; + } + + public void setGenTime(Long genTime) { + this.genTime = genTime; + } + + @Override + public String toString() { + return userCode + "_" + genTime + "_" + expTime; + } +} diff --git a/src/main/java/com/keyware/regtool/WebViewController.java b/src/main/java/com/keyware/regtool/WebViewController.java new file mode 100644 index 0000000..47fb6f1 --- /dev/null +++ b/src/main/java/com/keyware/regtool/WebViewController.java @@ -0,0 +1,72 @@ +package com.keyware.regtool; + +import cn.hutool.core.date.DateField; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.util.StrUtil; + +import java.util.Date; + +public class WebViewController { + private final static String key = "com.keyware.123"; + private RegInfo regInfo; + + /** + * 验证用户码 + * + * @param userCode 用户码 + * @return 是否有效 + */ + public boolean verifyUserCode(String userCode) { + String code = AESUtil.stringAesDecrypt(userCode, key); + if(StrUtil.isNotBlank(code)){ + regInfo = new RegInfo(code); + return true; + } + return false; + } + + /** + * 生成注册码 + * + * @param duration 时效 + * @param unit 单位,d:天,m:月,y:年 + * @return 注册码 + */ + public String genRegCode(int duration, String unit) { + if(regInfo != null){ + regInfo.setGenTime(new Date().getTime()); + long expDate = getExpTime(duration, unit); + if(expDate == 0){ + return ""; + } + regInfo.setExpTime(expDate); + return AESUtil.stringAesEncrypt(regInfo.toString(), key); + } + return ""; + } + + private long getExpTime(int duration, String unit){ + long regTime = this.regInfo.getGenTime(); + Date date = new Date(regTime); + Date expDate = null; + switch (unit){ + case "d": + expDate = DateUtil.offsetDay(date, duration); + break; + case "m": + expDate = DateUtil.offsetMonth(date, duration); + break; + case "y": + expDate = DateUtil.offset(date, DateField.YEAR, duration); + break; + case "e": + expDate = DateUtil.offset(date, DateField.YEAR, 999); + break; + } + System.out.println("duration = " + duration + ", unit = " + unit + ", expDate="+DateUtil.formatDate(expDate)); + if(expDate != null){ + return expDate.getTime(); + } + return 0; + } +} \ No newline at end of file diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java new file mode 100644 index 0000000..a5dbc4a --- /dev/null +++ b/src/main/java/module-info.java @@ -0,0 +1,10 @@ +module com.keyware.regtool { + requires javafx.controls; + requires javafx.fxml; + requires javafx.web; + + requires jdk.jsobject; + + opens com.keyware.regtool to javafx.web; + exports com.keyware.regtool; +} \ No newline at end of file diff --git a/src/main/resources/META-INF/MANIFEST.MF b/src/main/resources/META-INF/MANIFEST.MF new file mode 100644 index 0000000..230958a --- /dev/null +++ b/src/main/resources/META-INF/MANIFEST.MF @@ -0,0 +1,3 @@ +Manifest-Version: 1.0 +Main-Class: com.keyware.regtool.Main + diff --git a/src/main/resources/logo.ico b/src/main/resources/logo.ico new file mode 100644 index 0000000000000000000000000000000000000000..e16ed2a3a2e32ac05564a0ed08c96e10ef942749 GIT binary patch literal 292878 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 Qe=zT)YGuvTU)sh018A>WLI3~& literal 0 HcmV?d00001 diff --git a/src/main/resources/logo.png b/src/main/resources/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..847e2627eedfd984c9d89198453f6fa6b0fe0d28 GIT binary patch literal 4064 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 nx{XItc9*{padding:.5px}.layui-col-space2{margin:-1px}.layui-col-space2>*{padding:1px}.layui-col-space4{margin:-2px}.layui-col-space4>*{padding:2px}.layui-col-space5{margin:-2.5px}.layui-col-space5>*{padding:2.5px}.layui-col-space6{margin:-3px}.layui-col-space6>*{padding:3px}.layui-col-space8{margin:-4px}.layui-col-space8>*{padding:4px}.layui-col-space10{margin:-5px}.layui-col-space10>*{padding:5px}.layui-col-space12{margin:-6px}.layui-col-space12>*{padding:6px}.layui-col-space14{margin:-7px}.layui-col-space14>*{padding:7px}.layui-col-space15{margin:-7.5px}.layui-col-space15>*{padding:7.5px}.layui-col-space16{margin:-8px}.layui-col-space16>*{padding:8px}.layui-col-space18{margin:-9px}.layui-col-space18>*{padding:9px}.layui-col-space20{margin:-10px}.layui-col-space20>*{padding:10px}.layui-col-space22{margin:-11px}.layui-col-space22>*{padding:11px}.layui-col-space24{margin:-12px}.layui-col-space24>*{padding:12px}.layui-col-space25{margin:-12.5px}.layui-col-space25>*{padding:12.5px}.layui-col-space26{margin:-13px}.layui-col-space26>*{padding:13px}.layui-col-space28{margin:-14px}.layui-col-space28>*{padding:14px}.layui-col-space30{margin:-15px}.layui-col-space30>*{padding:15px}.layui-col-space32{margin:-16px}.layui-col-space32>*{padding:16px}.layui-padding-1{padding:4px!important}.layui-padding-2{padding:8px!important}.layui-padding-3{padding:16px!important}.layui-padding-4{padding:32px!important}.layui-padding-5{padding:48px!important}.layui-margin-1{margin:4px!important}.layui-margin-2{margin:8px!important}.layui-margin-3{margin:16px!important}.layui-margin-4{margin:32px!important}.layui-margin-5{margin:48px!important}.layui-btn,.layui-input,.layui-select,.layui-textarea,.layui-upload-button{outline:0;-webkit-appearance:none;transition:all .3s;-webkit-transition:all .3s;box-sizing:border-box}.layui-elem-quote{margin-bottom:10px;padding:15px;line-height:1.8;border-left:5px solid #16b777;border-radius:0 2px 2px 0;background-color:#fafafa}.layui-quote-nm{border-style:solid;border-width:1px;border-left-width:5px;background:0 0}.layui-elem-field{margin-bottom:10px;padding:0;border-width:1px;border-style:solid}.layui-elem-field legend{margin-left:20px;padding:0 10px;font-size:20px}.layui-field-title{margin:16px 0;border-width:0;border-top-width:1px}.layui-field-box{padding:15px}.layui-field-title .layui-field-box{padding:10px 0}.layui-progress{position:relative;height:6px;border-radius:20px;background-color:#eee}.layui-progress-bar{position:absolute;left:0;top:0;width:0;max-width:100%;height:6px;border-radius:20px;text-align:right;background-color:#16b777;transition:all .3s;-webkit-transition:all .3s}.layui-progress-big,.layui-progress-big .layui-progress-bar{height:18px;line-height:18px}.layui-progress-text{position:relative;top:-20px;line-height:18px;font-size:12px;color:#5f5f5f}.layui-progress-big .layui-progress-text{position:static;padding:0 10px;color:#fff}.layui-collapse{border-width:1px;border-style:solid;border-radius:2px}.layui-colla-content,.layui-colla-item{border-top-width:1px;border-top-style:solid}.layui-colla-item:first-child{border-top:none}.layui-colla-title{position:relative;height:42px;line-height:42px;padding:0 15px 0 35px;color:#333;background-color:#fafafa;cursor:pointer;font-size:14px;overflow:hidden}.layui-colla-content{display:none;padding:10px 15px;line-height:1.6;color:#5f5f5f}.layui-colla-icon{position:absolute;left:15px;top:0;font-size:14px}.layui-card{margin-bottom:15px;border-radius:2px;background-color:#fff;box-shadow:0 1px 2px 0 rgba(0,0,0,.05)}.layui-card:last-child{margin-bottom:0}.layui-card-header{position:relative;height:42px;line-height:42px;padding:0 15px;border-bottom:1px solid #f8f8f8;color:#333;border-radius:2px 2px 0 0;font-size:14px}.layui-card-body{position:relative;padding:10px 15px;line-height:24px}.layui-card-body[pad15]{padding:15px}.layui-card-body[pad20]{padding:20px}.layui-card-body .layui-table{margin:5px 0}.layui-card .layui-tab{margin:0}.layui-panel{position:relative;border-width:1px;border-style:solid;border-radius:2px;box-shadow:1px 1px 4px rgb(0 0 0 / 8%);background-color:#fff;color:#5f5f5f}.layui-panel-window{position:relative;padding:15px;border-radius:0;border-top:5px solid #eee;background-color:#fff}.layui-auxiliar-moving{position:fixed;left:0;right:0;top:0;bottom:0;width:100%;height:100%;background:0 0;z-index:9999999999}.layui-scrollbar-hide{overflow:hidden!important}.layui-bg-red{background-color:#ff5722!important;color:#fff!important}.layui-bg-orange{background-color:#ffb800!important;color:#fff!important}.layui-bg-green{background-color:#16baaa!important;color:#fff!important}.layui-bg-cyan{background-color:#2f4056!important;color:#fff!important}.layui-bg-blue{background-color:#1e9fff!important;color:#fff!important}.layui-bg-purple{background-color:#a233c6!important;color:#fff!important}.layui-bg-black{background-color:#2f363c!important;color:#fff!important}.layui-bg-gray{background-color:#fafafa!important;color:#5f5f5f!important}.layui-badge-rim,.layui-border,.layui-colla-content,.layui-colla-item,.layui-collapse,.layui-elem-field,.layui-form-pane .layui-form-item[pane],.layui-form-pane .layui-form-label,.layui-input,.layui-input-split,.layui-panel,.layui-quote-nm,.layui-select,.layui-tab-bar,.layui-tab-card,.layui-tab-title,.layui-tab-title .layui-this:after,.layui-textarea{border-color:#eee}.layui-border{border-width:1px;border-style:solid;color:#5f5f5f!important}.layui-border-red{border-width:1px;border-style:solid;border-color:#ff5722!important;color:#ff5722!important}.layui-border-orange{border-width:1px;border-style:solid;border-color:#ffb800!important;color:#ffb800!important}.layui-border-green{border-width:1px;border-style:solid;border-color:#16baaa!important;color:#16baaa!important}.layui-border-cyan{border-width:1px;border-style:solid;border-color:#2f4056!important;color:#2f4056!important}.layui-border-blue{border-width:1px;border-style:solid;border-color:#1e9fff!important;color:#1e9fff!important}.layui-border-purple{border-width:1px;border-style:solid;border-color:#a233c6!important;color:#a233c6!important}.layui-border-black{border-width:1px;border-style:solid;border-color:#2f363c!important;color:#2f363c!important}hr.layui-border-black,hr.layui-border-blue,hr.layui-border-cyan,hr.layui-border-green,hr.layui-border-orange,hr.layui-border-purple,hr.layui-border-red{border-width:0 0 1px}.layui-timeline-item:before{background-color:#eee}.layui-text{line-height:1.8;font-size:14px}.layui-text h1,.layui-text h2,.layui-text h3,.layui-text h4,.layui-text h5,.layui-text h6{color:#3a3a3a}.layui-text h1{font-size:32px}.layui-text h2{font-size:24px}.layui-text h3{font-size:18px}.layui-text h4{font-size:16px}.layui-text h5{font-size:14px}.layui-text h6{font-size:13px}.layui-text ol,.layui-text ul{padding-left:15px}.layui-text ul li{margin-top:5px;list-style-type:disc}.layui-text ol li{margin-top:5px;list-style-type:decimal}.layui-text-em,.layui-word-aux{color:#999!important;padding-left:5px!important;padding-right:5px!important}.layui-text p{margin:15px 0}.layui-text p:first-child{margin-top:0}.layui-text p:last-child{margin-bottom:0}.layui-text a:not(.layui-btn){color:#01aaed}.layui-text a:not(.layui-btn):hover{text-decoration:underline}.layui-text blockquote:not(.layui-elem-quote){padding:5px 15px;border-left:5px solid #eee}.layui-text pre>code:not(.layui-code){padding:15px;font-family:"Courier New",Consolas,"Lucida Console"}.layui-font-12{font-size:12px!important}.layui-font-13{font-size:13px!important}.layui-font-14{font-size:14px!important}.layui-font-16{font-size:16px!important}.layui-font-18{font-size:18px!important}.layui-font-20{font-size:20px!important}.layui-font-22{font-size:22px!important}.layui-font-24{font-size:24px!important}.layui-font-26{font-size:26px!important}.layui-font-28{font-size:28px!important}.layui-font-30{font-size:30px!important}.layui-font-32{font-size:32px!important}.layui-font-red{color:#ff5722!important}.layui-font-orange{color:#ffb800!important}.layui-font-green{color:#16baaa!important}.layui-font-cyan{color:#2f4056!important}.layui-font-blue{color:#01aaed!important}.layui-font-purple{color:#a233c6!important}.layui-font-black{color:#000!important}.layui-font-gray{color:#c2c2c2!important}.layui-btn{display:inline-block;vertical-align:middle;height:38px;line-height:38px;border:1px solid transparent;padding:0 18px;background-color:#16baaa;color:#fff;white-space:nowrap;text-align:center;font-size:14px;border-radius:2px;cursor:pointer;-moz-user-select:none;-webkit-user-select:none;-ms-user-select:none}.layui-btn:hover{opacity:.8;filter:alpha(opacity=80);color:#fff}.layui-btn:active{opacity:1;filter:alpha(opacity=100)}.layui-btn+.layui-btn{margin-left:10px}.layui-btn-container{word-spacing:-5px}.layui-btn-container .layui-btn{margin-right:10px;margin-bottom:10px;word-spacing:normal}.layui-btn-container .layui-btn+.layui-btn{margin-left:0}.layui-table .layui-btn-container .layui-btn{margin-bottom:9px}.layui-btn-radius{border-radius:100px}.layui-btn .layui-icon{padding:0 2px;vertical-align:middle\0;vertical-align:bottom}.layui-btn-primary{border-color:#d2d2d2;background:0 0;color:#5f5f5f}.layui-btn-primary:hover{border-color:#16baaa;color:#333}.layui-btn-normal{background-color:#1e9fff}.layui-btn-warm{background-color:#ffb800}.layui-btn-danger{background-color:#ff5722}.layui-btn-checked{background-color:#16b777}.layui-btn-disabled,.layui-btn-disabled:active,.layui-btn-disabled:hover{border-color:#eee!important;background-color:#fbfbfb!important;color:#d2d2d2!important;cursor:not-allowed!important;opacity:1}.layui-btn-lg{height:44px;line-height:44px;padding:0 25px;font-size:16px}.layui-btn-sm{height:30px;line-height:30px;padding:0 10px;font-size:12px}.layui-btn-xs{height:22px;line-height:22px;padding:0 5px;font-size:12px}.layui-btn-xs i{font-size:12px!important}.layui-btn-group{display:inline-block;vertical-align:middle;font-size:0}.layui-btn-group .layui-btn{margin-left:0!important;margin-right:0!important;border-left:1px solid rgba(255,255,255,.5);border-radius:0}.layui-btn-group .layui-btn-primary{border-left:none}.layui-btn-group .layui-btn-primary:hover{border-color:#d2d2d2;color:#16baaa}.layui-btn-group .layui-btn:first-child{border-left:none;border-radius:2px 0 0 2px}.layui-btn-group .layui-btn-primary:first-child{border-left:1px solid #d2d2d2}.layui-btn-group .layui-btn:last-child{border-radius:0 2px 2px 0}.layui-btn-group .layui-btn+.layui-btn{margin-left:0}.layui-btn-group+.layui-btn-group{margin-left:10px}.layui-btn-fluid{width:100%}.layui-input,.layui-select,.layui-textarea{height:38px;line-height:1.3;line-height:38px\9;border-width:1px;border-style:solid;background-color:#fff;color:rgba(0,0,0,.85);border-radius:2px}.layui-input::-webkit-input-placeholder,.layui-select::-webkit-input-placeholder,.layui-textarea::-webkit-input-placeholder{line-height:1.3}.layui-input,.layui-textarea{display:block;width:100%;padding-left:10px}.layui-input:hover,.layui-textarea:hover{border-color:#d2d2d2!important}.layui-input:focus,.layui-textarea:focus{border-color:#16b777!important;box-shadow:0 0 0 3px rgba(22,183,119,.08)}.layui-textarea{position:relative;min-height:100px;height:auto;line-height:20px;padding:6px 10px;resize:vertical}.layui-input[disabled],.layui-textarea[disabled]{background-color:#fafafa}.layui-select{padding:0 10px}.layui-form input[type=checkbox],.layui-form input[type=radio],.layui-form select{display:none}.layui-form [lay-ignore]{display:initial}.layui-form-item{position:relative;margin-bottom:15px;clear:both;*zoom:1}.layui-form-item:after{content:'\20';clear:both;*zoom:1;display:block;height:0}.layui-form-label{position:relative;float:left;display:block;padding:9px 15px;width:80px;font-weight:400;line-height:20px;text-align:right}.layui-form-label-col{display:block;float:none;padding:9px 0;line-height:20px;text-align:left}.layui-form-item .layui-inline{margin-bottom:5px;margin-right:10px}.layui-input-block,.layui-input-inline{position:relative}.layui-input-block{margin-left:110px;min-height:36px}.layui-input-inline{display:inline-block;vertical-align:middle}.layui-form-item .layui-input-inline{float:left;width:190px;margin-right:10px}.layui-form-text .layui-input-inline{width:auto}.layui-form-mid{position:relative;float:left;display:block;padding:9px 0!important;line-height:20px;margin-right:10px}.layui-form-danger+.layui-form-select .layui-input,.layui-form-danger:focus{border-color:#ff5722!important;box-shadow:0 0 0 3px rgba(255,87,34,.08)}.layui-input-prefix,.layui-input-split,.layui-input-suffix,.layui-input-suffix .layui-input-affix{position:absolute;right:0;top:0;padding:0 10px;width:35px;height:100%;text-align:center;transition:all .3s;box-sizing:border-box}.layui-input-prefix{left:0;border-radius:2px 0 0 2px}.layui-input-suffix{right:0;border-radius:0 2px 2px 0}.layui-input-split{border-width:1px;border-style:solid}.layui-input-prefix .layui-icon,.layui-input-split .layui-icon,.layui-input-suffix .layui-icon{position:relative;font-size:16px;color:#5f5f5f;transition:all .3s}.layui-input-group{position:relative;display:table;box-sizing:border-box}.layui-input-group>*{display:table-cell;vertical-align:middle;position:relative}.layui-input-group .layui-input{padding-right:15px}.layui-input-group>.layui-input-prefix{width:auto;border-right:0}.layui-input-group>.layui-input-suffix{width:auto;border-left:0}.layui-input-group .layui-input-split{white-space:nowrap}.layui-input-wrap{position:relative;line-height:38px}.layui-input-wrap .layui-input{padding-right:35px}.layui-input-wrap .layui-input::-ms-clear,.layui-input-wrap .layui-input::-ms-reveal{display:none}.layui-input-wrap .layui-input-prefix+.layui-input,.layui-input-wrap .layui-input-prefix~* .layui-input{padding-left:35px}.layui-input-wrap .layui-input-split+.layui-input,.layui-input-wrap .layui-input-split~* .layui-input{padding-left:45px}.layui-input-wrap .layui-input-prefix~.layui-form-select{position:static}.layui-input-wrap .layui-input-prefix,.layui-input-wrap .layui-input-split,.layui-input-wrap .layui-input-suffix{pointer-events:none}.layui-input-wrap .layui-input:hover+.layui-input-split{border-color:#d2d2d2}.layui-input-wrap .layui-input:focus+.layui-input-split{border-color:#16b777}.layui-input-wrap .layui-input.layui-form-danger:focus+.layui-input-split{border-color:#ff5722}.layui-input-wrap .layui-input-prefix.layui-input-split{border-width:0;border-right-width:1px}.layui-input-affix{line-height:38px}.layui-input-suffix .layui-input-affix{right:auto;left:-35px}.layui-input-affix .layui-icon{color:rgba(0,0,0,.8);pointer-events:auto!important;cursor:pointer}.layui-input-affix .layui-icon-clear{color:rgba(0,0,0,.3)}.layui-input-affix .layui-icon:hover{color:rgba(0,0,0,.6)}.layui-input-wrap .layui-input-number{width:24px;padding:0}.layui-input-wrap .layui-input-number .layui-icon{position:absolute;right:0;width:100%;height:50%;line-height:normal;font-size:12px}.layui-input-wrap .layui-input-number .layui-icon:before{position:absolute;left:50%;top:50%;margin-top:-6px;margin-left:-6px}.layui-input-wrap .layui-input-number .layui-icon-up{top:0;border-bottom:1px solid #eee}.layui-input-wrap .layui-input-number .layui-icon-down{bottom:0}.layui-input-wrap .layui-input-number .layui-icon:hover{font-weight:700}.layui-input-wrap .layui-input[type=number]::-webkit-inner-spin-button,.layui-input-wrap .layui-input[type=number]::-webkit-outer-spin-button{-webkit-appearance:none!important}.layui-input-wrap .layui-input[type=number]{-moz-appearance:textfield}.layui-input-wrap .layui-input[type=number].layui-input-number-out-of-range{color:#ff5722}.layui-form-select{position:relative;color:#5f5f5f}.layui-form-select .layui-input{padding-right:30px;cursor:pointer}.layui-form-select .layui-edge{position:absolute;right:10px;top:50%;margin-top:-3px;cursor:pointer;border-width:6px;border-top-color:#c2c2c2;border-top-style:solid;transition:all .3s;-webkit-transition:all .3s}.layui-form-select dl{display:none;position:absolute;left:0;top:42px;padding:5px 0;z-index:899;min-width:100%;border:1px solid #eee;max-height:300px;overflow-y:auto;background-color:#fff;border-radius:2px;box-shadow:1px 1px 4px rgb(0 0 0 / 8%);box-sizing:border-box}.layui-form-select dl dd,.layui-form-select dl dt{padding:0 10px;line-height:36px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.layui-form-select dl dt{font-size:12px;color:#999}.layui-form-select dl dd{cursor:pointer}.layui-form-select dl dd:hover{background-color:#f8f8f8;-webkit-transition:.5s all;transition:.5s all}.layui-form-select .layui-select-group dd{padding-left:20px}.layui-form-select dl dd.layui-select-tips{padding-left:10px!important;color:#999}.layui-form-select dl dd.layui-this{background-color:#f8f8f8;color:#16b777;font-weight:700}.layui-form-select dl dd.layui-disabled{background-color:#fff}.layui-form-selected dl{display:block}.layui-form-selected .layui-edge{margin-top:-9px;-webkit-transform:rotate(180deg);transform:rotate(180deg)}.layui-form-selected .layui-edge{margin-top:-3px\0}:root .layui-form-selected .layui-edge{margin-top:-9px\0/IE9}.layui-form-selectup dl{top:auto;bottom:42px}.layui-select-none{margin:5px 0;text-align:center;color:#999}.layui-select-disabled .layui-disabled{border-color:#eee!important}.layui-select-disabled .layui-edge{border-top-color:#d2d2d2}.layui-form-checkbox{position:relative;display:inline-block;vertical-align:middle;height:30px;line-height:30px;margin-right:10px;padding-right:30px;background-color:#fff;cursor:pointer;font-size:0;-webkit-transition:.1s linear;transition:.1s linear;box-sizing:border-box}.layui-form-checkbox>*{display:inline-block;vertical-align:middle}.layui-form-checkbox>div{padding:0 11px;font-size:14px;border-radius:2px 0 0 2px;background-color:#d2d2d2;color:#fff;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}.layui-form-checkbox>div>.layui-icon{line-height:normal}.layui-form-checkbox:hover>div{background-color:#c2c2c2}.layui-form-checkbox>i{position:absolute;right:0;top:0;width:30px;height:100%;border:1px solid #d2d2d2;border-left:none;border-radius:0 2px 2px 0;color:#fff;color:rgba(255,255,255,0);font-size:20px;text-align:center;box-sizing:border-box}.layui-form-checkbox:hover>i{border-color:#c2c2c2;color:#c2c2c2}.layui-form-checked,.layui-form-checked:hover{border-color:#16b777}.layui-form-checked:hover>div,.layui-form-checked>div{background-color:#16b777}.layui-form-checked:hover>i,.layui-form-checked>i{color:#16b777}.layui-form-item .layui-form-checkbox{margin-top:4px}.layui-form-checkbox.layui-checkbox-disabled>div{background-color:#eee!important}.layui-form [lay-checkbox]{display:none}.layui-form-checkbox[lay-skin=primary]{height:auto!important;line-height:normal!important;min-width:18px;min-height:18px;border:none!important;margin-right:0;padding-left:24px;padding-right:0;background:0 0}.layui-form-checkbox[lay-skin=primary]>div{margin-top:-1px;padding-left:0;padding-right:15px;line-height:18px;background:0 0;color:#5f5f5f}.layui-form-checkbox[lay-skin=primary]>i{right:auto;left:0;width:16px;height:16px;line-height:14px;border:1px solid #d2d2d2;font-size:12px;border-radius:2px;background-color:#fff;-webkit-transition:.1s linear;transition:.1s linear}.layui-form-checkbox[lay-skin=primary]:hover>i{border-color:#16b777;color:#fff}.layui-form-checked[lay-skin=primary]>i{border-color:#16b777!important;background-color:#16b777;color:#fff}.layui-checkbox-disabled[lay-skin=primary]>div{background:0 0!important}.layui-form-checked.layui-checkbox-disabled[lay-skin=primary]>i{background:#eee!important;border-color:#eee!important}.layui-checkbox-disabled[lay-skin=primary]:hover>i{border-color:#d2d2d2}.layui-form-item .layui-form-checkbox[lay-skin=primary]{margin-top:10px}.layui-form-checkbox[lay-skin=primary]>.layui-icon-indeterminate{border-color:#16b777}.layui-form-checkbox[lay-skin=primary]>.layui-icon-indeterminate:before{content:'';display:inline-block;vertical-align:middle;position:relative;width:50%;height:1px;margin:-1px auto 0;background-color:#16b777}.layui-form-switch{position:relative;display:inline-block;vertical-align:middle;height:24px;line-height:22px;min-width:44px;padding:0 5px;margin-top:8px;border:1px solid #d2d2d2;border-radius:20px;cursor:pointer;box-sizing:border-box;background-color:#fff;-webkit-transition:.1s linear;transition:.1s linear}.layui-form-switch>i{position:absolute;left:5px;top:3px;width:16px;height:16px;border-radius:20px;background-color:#d2d2d2;-webkit-transition:.1s linear;transition:.1s linear}.layui-form-switch>div{position:relative;top:0;margin-left:21px;padding:0!important;text-align:center!important;color:#999!important;font-style:normal!important;font-size:12px}.layui-form-onswitch{border-color:#16b777;background-color:#16b777}.layui-form-onswitch>i{left:100%;margin-left:-21px;background-color:#fff}.layui-form-onswitch>div{margin-left:0;margin-right:21px;color:#fff!important}.layui-checkbox-disabled{border-color:#eee!important}.layui-checkbox-disabled>div{color:#c2c2c2!important}.layui-checkbox-disabled>i{border-color:#eee!important}.layui-checkbox-disabled:hover>i{color:#fff!important}.layui-form-radio{display:inline-block;vertical-align:middle;line-height:28px;margin:6px 10px 0 0;padding-right:10px;cursor:pointer;font-size:0}.layui-form-radio>*{display:inline-block;vertical-align:middle;font-size:14px}.layui-form-radio>i{margin-right:8px;font-size:22px;color:#c2c2c2}.layui-form-radio:hover>*,.layui-form-radioed,.layui-form-radioed>i{color:#16b777}.layui-radio-disabled>i{color:#eee!important}.layui-radio-disabled>*{color:#c2c2c2!important}.layui-form [lay-radio]{display:none}.layui-form-pane .layui-form-label{width:110px;padding:8px 15px;height:38px;line-height:20px;border-width:1px;border-style:solid;border-radius:2px 0 0 2px;text-align:center;background-color:#fafafa;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;box-sizing:border-box}.layui-form-pane .layui-input-inline{margin-left:-1px}.layui-form-pane .layui-input-block{margin-left:110px;left:-1px}.layui-form-pane .layui-input{border-radius:0 2px 2px 0}.layui-form-pane .layui-form-text .layui-form-label{float:none;width:100%;border-radius:2px;box-sizing:border-box;text-align:left}.layui-form-pane .layui-form-text .layui-input-inline{display:block;margin:0;top:-1px;clear:both}.layui-form-pane .layui-form-text .layui-input-block{margin:0;left:0;top:-1px}.layui-form-pane .layui-form-text .layui-textarea{min-height:100px;border-radius:0 0 2px 2px}.layui-form-pane .layui-form-checkbox{margin:4px 0 4px 10px}.layui-form-pane .layui-form-radio,.layui-form-pane .layui-form-switch{margin-top:6px;margin-left:10px}.layui-form-pane .layui-form-item[pane]{position:relative;border-width:1px;border-style:solid}.layui-form-pane .layui-form-item[pane] .layui-form-label{position:absolute;left:0;top:0;height:100%;border-width:0;border-right-width:1px}.layui-form-pane .layui-form-item[pane] .layui-input-inline{margin-left:110px}@media screen and (max-width:450px){.layui-form-item .layui-form-label{text-overflow:ellipsis;overflow:hidden;white-space:nowrap}.layui-form-item .layui-inline{display:block;margin-right:0;margin-bottom:20px;clear:both}.layui-form-item .layui-inline:after{content:'\20';clear:both;display:block;height:0}.layui-form-item .layui-input-inline{display:block;float:none;left:-3px;width:auto!important;margin:0 0 10px 112px}.layui-form-item .layui-input-inline+.layui-form-mid{margin-left:110px;top:-5px;padding:0}.layui-form-item .layui-form-checkbox{margin-right:5px;margin-bottom:5px}}.layui-laypage{display:inline-block;*display:inline;*zoom:1;vertical-align:middle;margin:10px 0;font-size:0}.layui-laypage>a:first-child,.layui-laypage>a:first-child em{border-radius:2px 0 0 2px}.layui-laypage>a:last-child,.layui-laypage>a:last-child em{border-radius:0 2px 2px 0}.layui-laypage>:first-child{margin-left:0!important}.layui-laypage>:last-child{margin-right:0!important}.layui-laypage a,.layui-laypage button,.layui-laypage input,.layui-laypage select,.layui-laypage span{border:1px solid #eee}.layui-laypage a,.layui-laypage span{display:inline-block;*display:inline;*zoom:1;vertical-align:middle;padding:0 15px;height:28px;line-height:28px;margin:0 -1px 5px 0;background-color:#fff;color:#333;font-size:12px}.layui-laypage a[data-page]{color:#333}.layui-laypage a{text-decoration:none!important;cursor:pointer}.layui-laypage a:hover{color:#16baaa}.layui-laypage em{font-style:normal}.layui-laypage .layui-laypage-spr{color:#999;font-weight:700}.layui-laypage .layui-laypage-curr{position:relative}.layui-laypage .layui-laypage-curr em{position:relative;color:#fff}.layui-laypage .layui-laypage-curr .layui-laypage-em{position:absolute;left:-1px;top:-1px;padding:1px;width:100%;height:100%;background-color:#16baaa}.layui-laypage-em{border-radius:2px}.layui-laypage-next em,.layui-laypage-prev em{font-family:Sim sun;font-size:16px}.layui-laypage .layui-laypage-count,.layui-laypage .layui-laypage-limits,.layui-laypage .layui-laypage-refresh,.layui-laypage .layui-laypage-skip{margin-left:10px;margin-right:10px;padding:0;border:none}.layui-laypage .layui-laypage-limits,.layui-laypage .layui-laypage-refresh{vertical-align:top}.layui-laypage .layui-laypage-refresh i{font-size:18px;cursor:pointer}.layui-laypage select{height:22px;padding:3px;border-radius:2px;cursor:pointer}.layui-laypage .layui-laypage-skip{height:30px;line-height:30px;color:#999}.layui-laypage button,.layui-laypage input{height:30px;line-height:30px;border-radius:2px;vertical-align:top;background-color:#fff;box-sizing:border-box}.layui-laypage input{display:inline-block;width:40px;margin:0 10px;padding:0 3px;text-align:center}.layui-laypage input:focus,.layui-laypage select:focus{border-color:#16baaa!important}.layui-laypage button{margin-left:10px;padding:0 10px;cursor:pointer}.layui-flow-more{margin:10px 0;text-align:center;color:#999;font-size:14px;clear:both}.layui-flow-more a{height:32px;line-height:32px}.layui-flow-more a *{display:inline-block;vertical-align:top}.layui-flow-more a cite{padding:0 20px;border-radius:3px;background-color:#eee;color:#333;font-style:normal}.layui-flow-more a cite:hover{opacity:.8}.layui-flow-more a i{font-size:30px;color:#737383}.layui-table{width:100%;margin:10px 0;background-color:#fff;color:#5f5f5f}.layui-table tr{transition:all .3s;-webkit-transition:all .3s}.layui-table th{text-align:left;font-weight:600}.layui-table-mend{background-color:#fff}.layui-table-click,.layui-table-hover,.layui-table[lay-even] tbody tr:nth-child(even){background-color:#f8f8f8}.layui-table-checked{background-color:#dbfbf0}.layui-table-checked.layui-table-click,.layui-table-checked.layui-table-hover{background-color:#abf8dd}.layui-table td,.layui-table th,.layui-table-col-set,.layui-table-fixed-r,.layui-table-grid-down,.layui-table-header,.layui-table-mend,.layui-table-page,.layui-table-tips-main,.layui-table-tool,.layui-table-total,.layui-table-view,.layui-table[lay-skin=line],.layui-table[lay-skin=row]{border-width:1px;border-style:solid;border-color:#eee}.layui-table td,.layui-table th{position:relative;padding:9px 15px;min-height:20px;line-height:20px;font-size:14px}.layui-table[lay-skin=line] td,.layui-table[lay-skin=line] th{border-width:0;border-bottom-width:1px}.layui-table[lay-skin=row] td,.layui-table[lay-skin=row] th{border-width:0;border-right-width:1px}.layui-table[lay-skin=nob] td,.layui-table[lay-skin=nob] th{border:none}.layui-table img{max-width:100px}.layui-table[lay-size=lg] td,.layui-table[lay-size=lg] th{padding-top:15px;padding-right:30px;padding-bottom:15px;padding-left:30px}.layui-table-view .layui-table[lay-size=lg] .layui-table-cell{height:50px;line-height:40px}.layui-table[lay-size=sm] td,.layui-table[lay-size=sm] th{padding-top:5px;padding-right:10px;padding-bottom:5px;padding-left:10px;font-size:12px}.layui-table-view .layui-table[lay-size=sm] .layui-table-cell{height:30px;line-height:20px;padding-top:5px;padding-left:11px;padding-right:11px}.layui-table[lay-data],.layui-table[lay-options]{display:none}.layui-table-box{position:relative;overflow:hidden}.layui-table-view{clear:both}.layui-table-view .layui-table{position:relative;width:auto;margin:0;border:0;border-collapse:separate}.layui-table-view .layui-table[lay-skin=line]{border-width:0;border-right-width:1px}.layui-table-view .layui-table[lay-skin=row]{border-width:0;border-bottom-width:1px}.layui-table-view .layui-table td,.layui-table-view .layui-table th{padding:0;border-top:none;border-left:none}.layui-table-view .layui-table th [lay-event],.layui-table-view .layui-table th.layui-unselect .layui-table-cell span{cursor:pointer}.layui-table-view .layui-table td,.layui-table-view .layui-table th span{cursor:default}.layui-table-view .layui-table td[data-edit]{cursor:text}.layui-table-view .layui-table td[data-edit]:hover:after{position:absolute;left:0;top:0;width:100%;height:100%;box-sizing:border-box;border:1px solid #16b777;pointer-events:none;content:""}.layui-table-view .layui-form-checkbox[lay-skin=primary] i{width:18px;height:18px;line-height:16px}.layui-table-view .layui-form-radio{line-height:0;padding:0}.layui-table-view .layui-form-radio>i{margin:0;font-size:20px}.layui-table-init{position:absolute;left:0;top:0;width:100%;height:100%;text-align:center;z-index:199}.layui-table-init .layui-icon{position:absolute;left:50%;top:50%;margin:-15px 0 0 -15px;font-size:30px;color:#c2c2c2}.layui-table-header{border-width:0;border-bottom-width:1px;overflow:hidden}.layui-table-header .layui-table{margin-bottom:-1px}.layui-table-column{position:relative;width:100%;min-height:41px;padding:8px 16px;border-width:0;border-bottom-width:1px}.layui-table-column .layui-btn-container{margin-bottom:-8px}.layui-table-column .layui-btn-container .layui-btn{margin-right:8px;margin-bottom:8px}.layui-table-tool .layui-inline[lay-event]{position:relative;width:26px;height:26px;padding:5px;line-height:16px;margin-right:10px;text-align:center;color:#333;border:1px solid #ccc;cursor:pointer;-webkit-transition:.5s all;transition:.5s all}.layui-table-tool .layui-inline[lay-event]:hover{border:1px solid #999}.layui-table-tool-temp{padding-right:120px}.layui-table-tool-self{position:absolute;right:17px;top:10px}.layui-table-tool .layui-table-tool-self .layui-inline[lay-event]{margin:0 0 0 10px}.layui-table-tool-panel{position:absolute;top:29px;left:-1px;z-index:399;padding:5px 0!important;min-width:150px;min-height:40px;border:1px solid #d2d2d2;text-align:left;overflow-y:auto;background-color:#fff;box-shadow:0 2px 4px rgba(0,0,0,.12)}.layui-table-tool-panel li{padding:0 10px;margin:0!important;line-height:30px;list-style-type:none!important;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;-webkit-transition:.5s all;transition:.5s all}.layui-table-tool-panel li .layui-form-checkbox[lay-skin=primary]{width:100%}.layui-table-tool-panel li:hover{background-color:#f8f8f8}.layui-table-tool-panel li .layui-form-checkbox[lay-skin=primary]{padding-left:28px}.layui-table-tool-panel li .layui-form-checkbox[lay-skin=primary] i{position:absolute;left:0;top:0}.layui-table-tool-panel li .layui-form-checkbox[lay-skin=primary] span{padding:0}.layui-table-tool .layui-table-tool-self .layui-table-tool-panel{left:auto;right:-1px}.layui-table-col-set{position:absolute;right:0;top:0;width:20px;height:100%;border-width:0;border-left-width:1px;background-color:#fff}.layui-table-sort{width:10px;height:20px;margin-left:5px;cursor:pointer!important}.layui-table-sort .layui-edge{position:absolute;left:5px;border-width:5px}.layui-table-sort .layui-table-sort-asc{top:3px;border-top:none;border-bottom-style:solid;border-bottom-color:#b2b2b2}.layui-table-sort .layui-table-sort-asc:hover{border-bottom-color:#5f5f5f}.layui-table-sort .layui-table-sort-desc{bottom:5px;border-bottom:none;border-top-style:solid;border-top-color:#b2b2b2}.layui-table-sort .layui-table-sort-desc:hover{border-top-color:#5f5f5f}.layui-table-sort[lay-sort=asc] .layui-table-sort-asc{border-bottom-color:#000}.layui-table-sort[lay-sort=desc] .layui-table-sort-desc{border-top-color:#000}.layui-table-cell{height:38px;line-height:28px;padding:6px 15px;position:relative;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;box-sizing:border-box}.layui-table-cell .layui-form-checkbox[lay-skin=primary]{top:-1px;padding:0}.layui-table-cell .layui-form-checkbox[lay-skin=primary]>div{padding-left:24px}.layui-table-cell .layui-table-link{color:#01aaed}.layui-table-cell .layui-btn{vertical-align:inherit}.layui-table-cell[align=center]{-webkit-box-pack:center}.layui-table-cell[align=right]{-webkit-box-pack:end}.laytable-cell-checkbox,.laytable-cell-numbers,.laytable-cell-radio,.laytable-cell-space{text-align:center;-webkit-box-pack:center}.layui-table-body{position:relative;overflow:auto;margin-right:-1px;margin-bottom:-1px}.layui-table-body .layui-none{line-height:26px;padding:30px 15px;text-align:center;color:#999}.layui-table-fixed{position:absolute;left:0;top:0;z-index:101}.layui-table-fixed .layui-table-body{overflow:hidden}.layui-table-fixed-l{box-shadow:1px 0 8px rgba(0,0,0,.08)}.layui-table-fixed-r{left:auto;right:-1px;border-width:0;border-left-width:1px;box-shadow:-1px 0 8px rgba(0,0,0,.08)}.layui-table-fixed-r .layui-table-header{position:relative;overflow:visible}.layui-table-mend{position:absolute;right:-49px;top:0;height:100%;width:50px;border-width:0;border-left-width:1px}.layui-table-tool{position:relative;width:100%;min-height:50px;line-height:30px;padding:10px 15px;border-width:0;border-bottom-width:1px}.layui-table-tool .layui-btn-container{margin-bottom:-10px}.layui-table-total{margin-bottom:-1px;border-width:0;border-top-width:1px;overflow:hidden}.layui-table-page{border-width:0;border-top-width:1px;margin-bottom:-1px;white-space:nowrap;overflow:hidden}.layui-table-page>div{height:26px}.layui-table-page .layui-laypage{margin:0}.layui-table-page .layui-laypage a,.layui-table-page .layui-laypage span{height:26px;line-height:26px;margin-bottom:10px;border:none;background:0 0}.layui-table-page .layui-laypage a,.layui-table-page .layui-laypage span.layui-laypage-curr{padding:0 12px}.layui-table-page .layui-laypage span{margin-left:0;padding:0}.layui-table-page .layui-laypage .layui-laypage-prev{margin-left:-11px!important}.layui-table-page .layui-laypage .layui-laypage-curr .layui-laypage-em{left:0;top:0;padding:0}.layui-table-page .layui-laypage button,.layui-table-page .layui-laypage input{height:26px;line-height:26px}.layui-table-page .layui-laypage input{width:40px}.layui-table-page .layui-laypage button{padding:0 10px}.layui-table-page select{height:18px}.layui-table-pagebar{float:right;line-height:23px}.layui-table-pagebar .layui-btn-sm{margin-top:-1px}.layui-table-pagebar .layui-btn-xs{margin-top:2px}.layui-table-view select[lay-ignore]{display:inline-block}.layui-table-patch .layui-table-cell{padding:0;width:30px}.layui-table-edit{position:absolute;left:0;top:0;z-index:189;min-width:100%;min-height:100%;padding:5px 14px;border-radius:0;box-shadow:1px 1px 20px rgba(0,0,0,.15);background-color:#fff}.layui-table-edit:focus{border-color:#16b777!important}input.layui-input.layui-table-edit{height:100%}select.layui-table-edit{padding:0 0 0 10px;border-color:#d2d2d2}.layui-table-view .layui-form-checkbox,.layui-table-view .layui-form-radio,.layui-table-view .layui-form-switch{top:0;margin:0}.layui-table-view .layui-form-checkbox{top:-1px;height:26px;line-height:26px}.layui-table-view .layui-form-checkbox i{height:26px}.layui-table-grid .layui-table-cell{overflow:visible}.layui-table-grid-down{position:absolute;top:0;right:0;width:24px;height:100%;padding:5px 0;border-width:0;border-left-width:1px;text-align:center;background-color:#fff;color:#999;cursor:pointer}.layui-table-grid-down .layui-icon{position:absolute;top:50%;left:50%;margin:-8px 0 0 -8px;font-size:14px}.layui-table-grid-down:hover{background-color:#fbfbfb}.layui-table-expanded{height:95px}.layui-table-expanded .layui-table-cell,.layui-table-view .layui-table[lay-size=lg] .layui-table-expanded .layui-table-cell,.layui-table-view .layui-table[lay-size=sm] .layui-table-expanded .layui-table-cell{height:auto;max-height:94px;white-space:normal;text-overflow:clip}.layui-table-cell-c{position:absolute;bottom:-10px;right:50%;margin-right:-9px;width:20px;height:20px;line-height:18px;cursor:pointer;text-align:center;background-color:#fff;border:1px solid #eee;border-radius:50%;z-index:1000;transition:.3s all;font-size:14px}.layui-table-cell-c:hover{border-color:#16b777}.layui-table-expanded td:hover .layui-table-cell{overflow:auto}body .layui-table-tips .layui-layer-content{background:0 0;padding:0;box-shadow:0 1px 6px rgba(0,0,0,.12)}.layui-table-tips-main{margin:-49px 0 0 -1px;max-height:150px;padding:8px 15px;font-size:14px;overflow-y:scroll;background-color:#fff;color:#5f5f5f}.layui-table-tips-c{position:absolute;right:-3px;top:-13px;width:20px;height:20px;padding:3px;cursor:pointer;background-color:#5f5f5f;border-radius:50%;color:#fff}.layui-table-tips-c:hover{background-color:#777}.layui-table-tips-c:before{position:relative;right:-2px}.layui-table-tree-nodeIcon{max-width:20px}.layui-table-tree-nodeIcon>*{width:100%}.layui-table-tree-flexIcon,.layui-table-tree-nodeIcon{margin-right:2px}.layui-table-tree-flexIcon{cursor:pointer}.layui-upload-file{display:none!important;opacity:.01;filter:Alpha(opacity=1)}.layui-upload-list{margin:11px 0}.layui-upload-choose{max-width:200px;padding:0 10px;color:#999;font-size:14px;text-overflow:ellipsis;overflow:hidden;white-space:nowrap}.layui-upload-drag{position:relative;display:inline-block;padding:30px;border:1px dashed #e2e2e2;background-color:#fff;text-align:center;cursor:pointer;color:#999}.layui-upload-drag .layui-icon{font-size:50px;color:#16baaa}.layui-upload-drag[lay-over]{border-color:#16baaa}.layui-upload-form{display:inline-block}.layui-upload-iframe{position:absolute;width:0;height:0;border:0;visibility:hidden}.layui-upload-wrap{position:relative;display:inline-block;vertical-align:middle}.layui-upload-wrap .layui-upload-file{display:block!important;position:absolute;left:0;top:0;z-index:10;font-size:100px;width:100%;height:100%;opacity:.01;filter:Alpha(opacity=1);cursor:pointer}.layui-btn-container .layui-upload-choose{padding-left:0}.layui-menu{position:relative;margin:5px 0;background-color:#fff;box-sizing:border-box}.layui-menu *{box-sizing:border-box}.layui-menu li,.layui-menu-body-title,.layui-menu-body-title a{padding:5px 15px;color:initial}.layui-menu li{position:relative;margin:0 0 1px;line-height:26px;color:rgba(0,0,0,.8);font-size:14px;white-space:nowrap;cursor:pointer;transition:all .3s}.layui-menu li:hover{background-color:#f8f8f8}.layui-menu li.layui-disabled,.layui-menu li.layui-disabled *{background:0 0!important;color:#d2d2d2!important;cursor:not-allowed!important}.layui-menu-item-parent:hover>.layui-menu-body-panel{display:block;animation-name:layui-fadein;animation-duration:.3s;animation-fill-mode:both;animation-delay:.2s}.layui-menu-item-group>.layui-menu-body-title,.layui-menu-item-parent>.layui-menu-body-title{padding-right:38px}.layui-menu .layui-menu-item-divider:hover,.layui-menu .layui-menu-item-group:hover,.layui-menu .layui-menu-item-none:hover{background:0 0;cursor:default}.layui-menu .layui-menu-item-group>ul{margin:5px 0 -5px}.layui-menu .layui-menu-item-group>.layui-menu-body-title{color:rgba(0,0,0,.35);user-select:none}.layui-menu .layui-menu-item-none{color:rgba(0,0,0,.35);cursor:default}.layui-menu .layui-menu-item-none{text-align:center}.layui-menu .layui-menu-item-divider{margin:5px 0;padding:0;height:0;line-height:0;border-bottom:1px solid #eee;overflow:hidden}.layui-menu .layui-menu-item-down:hover,.layui-menu .layui-menu-item-up:hover{cursor:pointer}.layui-menu .layui-menu-item-up>.layui-menu-body-title{color:rgba(0,0,0,.8)}.layui-menu .layui-menu-item-up>ul{visibility:hidden;height:0;overflow:hidden}.layui-menu .layui-menu-item-down>.layui-menu-body-title>.layui-icon-down{transform:rotate(180deg)}.layui-menu .layui-menu-item-up>.layui-menu-body-title>.layui-icon-up{transform:rotate(-180deg)}.layui-menu .layui-menu-item-down:hover>.layui-menu-body-title>.layui-icon,.layui-menu .layui-menu-item-up>.layui-menu-body-title:hover>.layui-icon{color:#000}.layui-menu .layui-menu-item-down>ul{visibility:visible;height:auto}.layui-menu .layui-menu-item-checked,.layui-menu .layui-menu-item-checked2{background-color:#f8f8f8!important;color:#16b777}.layui-menu .layui-menu-item-checked a,.layui-menu .layui-menu-item-checked2 a{color:#16b777}.layui-menu .layui-menu-item-checked:after{position:absolute;right:-1px;top:0;bottom:0;border-right:3px solid #16b777;content:""}.layui-menu-body-title{position:relative;margin:-5px -15px;overflow:hidden;text-overflow:ellipsis}.layui-menu-body-title a{display:block;margin:-5px -15px;color:rgba(0,0,0,.8)}.layui-menu-body-title a:hover{transition:all .3s}.layui-menu-body-title>.layui-icon{position:absolute;right:15px;top:50%;margin-top:-6px;line-height:normal;font-size:14px;transition:all .2s;-webkit-transition:all .2s}.layui-menu-body-title>.layui-icon:hover{transition:all .3s}.layui-menu-body-title>.layui-icon-right{right:14px}.layui-menu-body-panel{display:none;position:absolute;top:-7px;left:100%;z-index:1000;margin-left:13px;padding:5px 0}.layui-menu-body-panel:before{content:"";position:absolute;width:20px;left:-16px;top:0;bottom:0}.layui-menu-body-panel-left{left:auto;right:100%;margin:0 13px 0}.layui-menu-body-panel-left:before{left:auto;right:-16px}.layui-menu-lg li{line-height:32px}.layui-menu-lg .layui-menu-body-title a:hover,.layui-menu-lg li:hover{background:0 0;color:#16b777}.layui-menu-lg li .layui-menu-body-panel{margin-left:14px}.layui-menu-lg li .layui-menu-body-panel-left{margin:0 15px 0}.layui-dropdown{position:absolute;left:-999999px;top:-999999px;z-index:77777777;margin:5px 0;min-width:100px}.layui-dropdown:before{content:"";position:absolute;width:100%;height:6px;left:0;top:-6px}.layui-dropdown-shade{top:0;left:0;width:100%;height:100%;_height:expression(document.body.offsetHeight+"px");position:fixed;_position:absolute;pointer-events:auto}.layui-nav{position:relative;padding:0 15px;background-color:#2f363c;color:#fff;border-radius:2px;font-size:0;box-sizing:border-box}.layui-nav *{font-size:14px}.layui-nav .layui-nav-item{position:relative;display:inline-block;*display:inline;*zoom:1;margin-top:0;list-style:none;vertical-align:middle;line-height:60px}.layui-nav .layui-nav-item a{display:block;padding:0 20px;color:#fff;color:rgba(255,255,255,.7);transition:all .3s;-webkit-transition:all .3s}.layui-nav .layui-this:after,.layui-nav-bar{content:"";position:absolute;left:0;top:0;width:0;height:3px;background-color:#16b777;transition:all .2s;-webkit-transition:all .2s;pointer-events:none}.layui-nav-bar{z-index:1000}.layui-nav[lay-bar=disabled] .layui-nav-bar{display:none}.layui-nav .layui-nav-item a:hover,.layui-nav .layui-this a{color:#fff;text-decoration:none}.layui-nav .layui-this:after{top:auto;bottom:0;width:100%}.layui-nav-img{width:30px;height:30px;margin-right:10px;border-radius:50%}.layui-nav .layui-nav-more{position:absolute;top:0;right:3px;left:auto!important;margin-top:0;font-size:12px;cursor:pointer;transition:all .2s;-webkit-transition:all .2s}.layui-nav .layui-nav-mored,.layui-nav-itemed>a .layui-nav-more{transform:rotate(180deg)}.layui-nav-child{display:none;position:absolute;left:0;top:65px;min-width:100%;line-height:36px;padding:5px 0;box-shadow:0 2px 4px rgba(0,0,0,.12);border:1px solid #eee;background-color:#fff;z-index:100;border-radius:2px;white-space:nowrap;box-sizing:border-box}.layui-nav .layui-nav-child a{color:#5f5f5f;color:rgba(0,0,0,.8)}.layui-nav .layui-nav-child a:hover{background-color:#f8f8f8;color:rgba(0,0,0,.8)}.layui-nav-child dd{margin:1px 0;position:relative}.layui-nav-child dd.layui-this{background-color:#f8f8f8;color:#000}.layui-nav-child dd.layui-this:after{display:none}.layui-nav-child-r{left:auto;right:0}.layui-nav-child-c{text-align:center}.layui-nav.layui-nav-tree{width:200px;padding:0}.layui-nav-tree .layui-nav-item{display:block;width:100%;line-height:40px}.layui-nav-tree .layui-nav-item a{position:relative;height:40px;line-height:40px;text-overflow:ellipsis;overflow:hidden;white-space:nowrap}.layui-nav-tree .layui-nav-item>a{padding-top:5px;padding-bottom:5px}.layui-nav-tree .layui-nav-more{right:15px}.layui-nav-tree .layui-nav-item>a .layui-nav-more{padding:5px 0}.layui-nav-tree .layui-nav-bar{width:5px;height:0}.layui-side .layui-nav-tree .layui-nav-bar{width:2px}.layui-nav-tree .layui-nav-child dd.layui-this,.layui-nav-tree .layui-nav-child dd.layui-this a,.layui-nav-tree .layui-this,.layui-nav-tree .layui-this>a,.layui-nav-tree .layui-this>a:hover{background-color:#16baaa;color:#fff}.layui-nav-tree .layui-this:after{display:none}.layui-nav-itemed>a,.layui-nav-tree .layui-nav-title a,.layui-nav-tree .layui-nav-title a:hover{color:#fff!important}.layui-nav-tree .layui-nav-bar{background-color:#16baaa}.layui-nav-tree .layui-nav-child{position:relative;z-index:0;top:0;border:none;background-color:rgba(0,0,0,.3);box-shadow:none}.layui-nav-tree .layui-nav-child dd{margin:0}.layui-nav-tree .layui-nav-child a{color:#fff;color:rgba(255,255,255,.7)}.layui-nav-tree .layui-nav-child a:hover{background:0 0;color:#fff}.layui-nav-itemed>.layui-nav-child,.layui-nav-itemed>.layui-nav-child>.layui-this>.layui-nav-child{display:block}.layui-nav-side{position:fixed;top:0;bottom:0;left:0;overflow-x:hidden;z-index:999}.layui-nav-tree.layui-bg-gray a,.layui-nav.layui-bg-gray .layui-nav-item a{color:#373737;color:rgba(0,0,0,.8)}.layui-nav-tree.layui-bg-gray .layui-nav-itemed>a{color:#000!important}.layui-nav.layui-bg-gray .layui-this a{color:#16b777}.layui-nav-tree.layui-bg-gray .layui-nav-child{padding-left:11px;background:0 0}.layui-nav-tree.layui-bg-gray .layui-nav-child dd.layui-this,.layui-nav-tree.layui-bg-gray .layui-nav-child dd.layui-this a,.layui-nav-tree.layui-bg-gray .layui-this,.layui-nav-tree.layui-bg-gray .layui-this>a{background:0 0!important;color:#16b777!important;font-weight:700}.layui-nav-tree.layui-bg-gray .layui-nav-bar{background-color:#16b777}.layui-breadcrumb{visibility:hidden;font-size:0}.layui-breadcrumb>*{font-size:14px}.layui-breadcrumb a{color:#999!important}.layui-breadcrumb a:hover{color:#16b777!important}.layui-breadcrumb a cite{color:#5f5f5f;font-style:normal}.layui-breadcrumb span[lay-separator]{margin:0 10px;color:#999}.layui-tab{margin:10px 0;text-align:left!important}.layui-tab[overflow]>.layui-tab-title{overflow:hidden}.layui-tab .layui-tab-title{position:relative;left:0;height:40px;white-space:nowrap;font-size:0;border-bottom-width:1px;border-bottom-style:solid;transition:all .2s;-webkit-transition:all .2s}.layui-tab .layui-tab-title li{display:inline-block;*display:inline;*zoom:1;vertical-align:middle;font-size:14px;transition:all .2s;-webkit-transition:all .2s}.layui-tab .layui-tab-title li{position:relative;line-height:40px;min-width:65px;margin:0;padding:0 15px;text-align:center;cursor:pointer}.layui-tab .layui-tab-title li a{display:block;padding:0 15px;margin:0 -15px}.layui-tab-title .layui-this{color:#000}.layui-tab-title .layui-this:after{position:absolute;left:0;top:0;content:"";width:100%;height:41px;border-width:1px;border-style:solid;border-bottom-color:#fff;border-radius:2px 2px 0 0;box-sizing:border-box;pointer-events:none}.layui-tab-bar{position:absolute;right:0;top:0;z-index:10;width:30px;height:39px;line-height:39px;border-width:1px;border-style:solid;border-radius:2px;text-align:center;background-color:#fff;cursor:pointer}.layui-tab-bar .layui-icon{position:relative;display:inline-block;top:3px;transition:all .3s;-webkit-transition:all .3s}.layui-tab-item{display:none}.layui-tab-more{padding-right:30px;height:auto!important;white-space:normal!important}.layui-tab-more li.layui-this:after{border-bottom-color:#eee;border-radius:2px}.layui-tab-more .layui-tab-bar .layui-icon{top:-2px;top:3px\0;-webkit-transform:rotate(180deg);transform:rotate(180deg)}:root .layui-tab-more .layui-tab-bar .layui-icon{top:-2px\0/IE9}.layui-tab-content{padding:15px 0}.layui-tab-title li .layui-tab-close{position:relative;display:inline-block;width:18px;height:18px;line-height:20px;margin-left:8px;top:1px;text-align:center;font-size:14px;color:#c2c2c2;transition:all .2s;-webkit-transition:all .2s}.layui-tab-title li .layui-tab-close:hover{border-radius:2px;background-color:#ff5722;color:#fff}.layui-tab-brief>.layui-tab-title .layui-this{color:#16baaa}.layui-tab-brief>.layui-tab-more li.layui-this:after,.layui-tab-brief>.layui-tab-title .layui-this:after{border:none;border-radius:0;border-bottom:2px solid #16b777}.layui-tab-brief[overflow]>.layui-tab-title .layui-this:after{top:-1px}.layui-tab-card{border-width:1px;border-style:solid;border-radius:2px;box-shadow:0 2px 5px 0 rgba(0,0,0,.1)}.layui-tab-card>.layui-tab-title{background-color:#fafafa}.layui-tab-card>.layui-tab-title li{margin-right:-1px;margin-left:-1px}.layui-tab-card>.layui-tab-title .layui-this{background-color:#fff}.layui-tab-card>.layui-tab-title .layui-this:after{border-top:none;border-width:1px;border-bottom-color:#fff}.layui-tab-card>.layui-tab-title .layui-tab-bar{height:40px;line-height:40px;border-radius:0;border-top:none;border-right:none}.layui-tab-card>.layui-tab-more .layui-this{background:0 0;color:#16b777}.layui-tab-card>.layui-tab-more .layui-this:after{border:none}.layui-timeline{padding-left:5px}.layui-timeline-item{position:relative;padding-bottom:20px}.layui-timeline-axis{position:absolute;left:-5px;top:0;z-index:10;width:20px;height:20px;line-height:20px;background-color:#fff;color:#16b777;border-radius:50%;text-align:center;cursor:pointer}.layui-timeline-axis:hover{color:#ff5722}.layui-timeline-item:before{content:"";position:absolute;left:5px;top:0;z-index:0;width:1px;height:100%}.layui-timeline-item:first-child:before{display:block}.layui-timeline-item:last-child:before{display:none}.layui-timeline-content{padding-left:25px}.layui-timeline-title{position:relative;margin-bottom:10px;line-height:22px}.layui-badge,.layui-badge-dot,.layui-badge-rim{position:relative;display:inline-block;padding:0 6px;font-size:12px;text-align:center;background-color:#ff5722;color:#fff;border-radius:2px}.layui-badge{height:18px;line-height:18px}.layui-badge-dot{width:8px;height:8px;padding:0;border-radius:50%}.layui-badge-rim{height:18px;line-height:18px;border-width:1px;border-style:solid;background-color:#fff;color:#5f5f5f}.layui-btn .layui-badge,.layui-btn .layui-badge-dot{margin-left:5px}.layui-nav .layui-badge,.layui-nav .layui-badge-dot{position:absolute;top:50%;margin:-5px 6px 0}.layui-nav .layui-badge{margin-top:-10px}.layui-tab-title .layui-badge,.layui-tab-title .layui-badge-dot{left:5px;top:-2px}.layui-carousel{position:relative;left:0;top:0;background-color:#f8f8f8}.layui-carousel>[carousel-item]{position:relative;width:100%;height:100%;overflow:hidden}.layui-carousel>[carousel-item]:before{position:absolute;content:'\e63d';left:50%;top:50%;width:100px;line-height:20px;margin:-10px 0 0 -50px;text-align:center;color:#c2c2c2;font-family:layui-icon!important;font-size:30px;font-style:normal;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.layui-carousel>[carousel-item]>*{display:none;position:absolute;left:0;top:0;width:100%;height:100%;background-color:#f8f8f8;transition-duration:.3s;-webkit-transition-duration:.3s}.layui-carousel-updown>*{-webkit-transition:.3s ease-in-out up;transition:.3s ease-in-out up}.layui-carousel-arrow{display:none\0;opacity:0;position:absolute;left:10px;top:50%;margin-top:-18px;width:36px;height:36px;line-height:36px;text-align:center;font-size:20px;border:none 0;border-radius:50%;background-color:rgba(0,0,0,.2);color:#fff;-webkit-transition-duration:.3s;transition-duration:.3s;cursor:pointer}.layui-carousel-arrow[lay-type=add]{left:auto!important;right:10px}.layui-carousel[lay-arrow=always] .layui-carousel-arrow{opacity:1;left:20px}.layui-carousel[lay-arrow=always] .layui-carousel-arrow[lay-type=add]{right:20px}.layui-carousel[lay-arrow=none] .layui-carousel-arrow{display:none}.layui-carousel-arrow:hover,.layui-carousel-ind ul:hover{background-color:rgba(0,0,0,.35)}.layui-carousel:hover .layui-carousel-arrow{display:block\0;opacity:1;left:20px}.layui-carousel:hover .layui-carousel-arrow[lay-type=add]{right:20px}.layui-carousel-ind{position:relative;top:-35px;width:100%;line-height:0!important;text-align:center;font-size:0}.layui-carousel[lay-indicator=outside]{margin-bottom:30px}.layui-carousel[lay-indicator=outside] .layui-carousel-ind{top:10px}.layui-carousel[lay-indicator=outside] .layui-carousel-ind ul{background-color:rgba(0,0,0,.5)}.layui-carousel[lay-indicator=none] .layui-carousel-ind{display:none}.layui-carousel-ind ul{display:inline-block;padding:5px;background-color:rgba(0,0,0,.2);border-radius:10px;-webkit-transition-duration:.3s;transition-duration:.3s}.layui-carousel-ind ul li{display:inline-block;width:10px;height:10px;margin:0 3px;font-size:14px;background-color:#eee;background-color:rgba(255,255,255,.5);border-radius:50%;cursor:pointer;-webkit-transition-duration:.3s;transition-duration:.3s}.layui-carousel-ind ul li:hover{background-color:rgba(255,255,255,.7)}.layui-carousel-ind ul li.layui-this{background-color:#fff}.layui-carousel>[carousel-item]>.layui-carousel-next,.layui-carousel>[carousel-item]>.layui-carousel-prev,.layui-carousel>[carousel-item]>.layui-this{display:block}.layui-carousel>[carousel-item]>.layui-this{left:0}.layui-carousel>[carousel-item]>.layui-carousel-prev{left:-100%}.layui-carousel>[carousel-item]>.layui-carousel-next{left:100%}.layui-carousel>[carousel-item]>.layui-carousel-next.layui-carousel-left,.layui-carousel>[carousel-item]>.layui-carousel-prev.layui-carousel-right{left:0}.layui-carousel>[carousel-item]>.layui-this.layui-carousel-left{left:-100%}.layui-carousel>[carousel-item]>.layui-this.layui-carousel-right{left:100%}.layui-carousel[lay-anim=updown] .layui-carousel-arrow{left:50%!important;top:20px;margin:0 0 0 -18px}.layui-carousel[lay-anim=updown] .layui-carousel-arrow[lay-type=add]{top:auto!important;bottom:20px}.layui-carousel[lay-anim=updown] .layui-carousel-ind{position:absolute;top:50%;right:20px;width:auto;height:auto}.layui-carousel[lay-anim=updown] .layui-carousel-ind ul{padding:3px 5px}.layui-carousel[lay-anim=updown] .layui-carousel-ind li{display:block;margin:6px 0}.layui-carousel[lay-anim=updown]>[carousel-item]>*{left:0!important}.layui-carousel[lay-anim=updown]>[carousel-item]>.layui-this{top:0}.layui-carousel[lay-anim=updown]>[carousel-item]>.layui-carousel-prev{top:-100%}.layui-carousel[lay-anim=updown]>[carousel-item]>.layui-carousel-next{top:100%}.layui-carousel[lay-anim=updown]>[carousel-item]>.layui-carousel-next.layui-carousel-left,.layui-carousel[lay-anim=updown]>[carousel-item]>.layui-carousel-prev.layui-carousel-right{top:0}.layui-carousel[lay-anim=updown]>[carousel-item]>.layui-this.layui-carousel-left{top:-100%}.layui-carousel[lay-anim=updown]>[carousel-item]>.layui-this.layui-carousel-right{top:100%}.layui-carousel[lay-anim=fade]>[carousel-item]>*{left:0!important}.layui-carousel[lay-anim=fade]>[carousel-item]>.layui-carousel-next,.layui-carousel[lay-anim=fade]>[carousel-item]>.layui-carousel-prev{opacity:0}.layui-carousel[lay-anim=fade]>[carousel-item]>.layui-carousel-next.layui-carousel-left,.layui-carousel[lay-anim=fade]>[carousel-item]>.layui-carousel-prev.layui-carousel-right{opacity:1}.layui-carousel[lay-anim=fade]>[carousel-item]>.layui-this.layui-carousel-left,.layui-carousel[lay-anim=fade]>[carousel-item]>.layui-this.layui-carousel-right{opacity:0}.layui-fixbar{position:fixed;right:16px;bottom:16px;z-index:999999}.layui-fixbar li{width:50px;height:50px;line-height:50px;margin-bottom:1px;text-align:center;cursor:pointer;font-size:30px;background-color:#9f9f9f;color:#fff;border-radius:2px;opacity:.95}.layui-fixbar li:hover{opacity:.85}.layui-fixbar li:active{opacity:1}.layui-fixbar .layui-fixbar-top{display:none;font-size:40px}body .layui-util-face{border:none;background:0 0}body .layui-util-face .layui-layer-content{padding:0;background-color:#fff;color:#5f5f5f;box-shadow:none}.layui-util-face .layui-layer-TipsG{display:none}.layui-util-face ul{position:relative;width:372px;padding:10px;border:1px solid #d9d9d9;background-color:#fff;box-shadow:0 0 20px rgba(0,0,0,.2)}.layui-util-face ul li{cursor:pointer;float:left;border:1px solid #e8e8e8;height:22px;width:26px;overflow:hidden;margin:-1px 0 0 -1px;padding:4px 2px;text-align:center}.layui-util-face ul li:hover{position:relative;z-index:2;border:1px solid #eb7350;background:#fff9ec}.layui-code{display:block;position:relative;padding:15px;line-height:20px;border:1px solid #eee;border-left-width:6px;background-color:#fff;color:#333;font-family:"Courier New",Consolas,"Lucida Console";font-size:12px}.layui-transfer-box,.layui-transfer-header,.layui-transfer-search{border-width:0;border-style:solid;border-color:#eee}.layui-transfer-box{position:relative;display:inline-block;vertical-align:middle;border-width:1px;width:200px;height:360px;border-radius:2px;background-color:#fff}.layui-transfer-box .layui-form-checkbox{width:100%;margin:0!important}.layui-transfer-header{height:38px;line-height:38px;padding:0 11px;border-bottom-width:1px}.layui-transfer-search{position:relative;padding:11px;border-bottom-width:1px}.layui-transfer-search .layui-input{height:32px;padding-left:30px;font-size:12px}.layui-transfer-search .layui-icon-search{position:absolute;left:20px;top:50%;line-height:normal;margin-top:-8px;color:#5f5f5f}.layui-transfer-active{margin:0 15px;display:inline-block;vertical-align:middle}.layui-transfer-active .layui-btn{display:block;margin:0;padding:0 15px;background-color:#16b777;border-color:#16b777;color:#fff}.layui-transfer-active .layui-btn-disabled{background-color:#fbfbfb;border-color:#eee;color:#d2d2d2}.layui-transfer-active .layui-btn:first-child{margin-bottom:15px}.layui-transfer-active .layui-btn .layui-icon{margin:0;font-size:14px!important}.layui-transfer-data{padding:5px 0;overflow:auto}.layui-transfer-data li{height:32px;line-height:32px;margin-top:0!important;padding:0 11px;list-style-type:none!important}.layui-transfer-data li:hover{background-color:#f8f8f8;transition:.5s all}.layui-transfer-data .layui-none{padding:15px 11px;text-align:center;color:#999}.layui-rate,.layui-rate *{display:inline-block;vertical-align:middle}.layui-rate{padding:11px 6px 11px 0;font-size:0}.layui-rate li{margin-top:0!important}.layui-rate li i.layui-icon{font-size:20px;color:#ffb800}.layui-rate li i.layui-icon{margin-right:5px;transition:all .3s;-webkit-transition:all .3s}.layui-rate li i:hover{cursor:pointer;transform:scale(1.12);-webkit-transform:scale(1.12)}.layui-rate[readonly] li i:hover{cursor:default;transform:scale(1)}.layui-colorpicker{width:38px;height:38px;border:1px solid #eee;padding:5px;border-radius:2px;line-height:24px;display:inline-block;cursor:pointer;transition:all .3s;-webkit-transition:all .3s;box-sizing:border-box}.layui-colorpicker:hover{border-color:#d2d2d2}.layui-colorpicker.layui-colorpicker-lg{width:44px;height:44px;line-height:30px}.layui-colorpicker.layui-colorpicker-sm{width:30px;height:30px;line-height:20px;padding:3px}.layui-colorpicker.layui-colorpicker-xs{width:22px;height:22px;line-height:16px;padding:1px}.layui-colorpicker-trigger-bgcolor{display:block;background:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAIAAADZF8uwAAAAGUlEQVQYV2M4gwH+YwCGIasIUwhT25BVBADtzYNYrHvv4gAAAABJRU5ErkJggg==);border-radius:2px}.layui-colorpicker-trigger-span{display:block;height:100%;box-sizing:border-box;border:1px solid rgba(0,0,0,.15);border-radius:2px;text-align:center}.layui-colorpicker-trigger-i{display:inline-block;color:#fff;font-size:12px}.layui-colorpicker-trigger-i.layui-icon-close{color:#999}.layui-colorpicker-main{position:absolute;left:-999999px;top:-999999px;z-index:77777777;width:280px;margin:5px 0;padding:7px;background:#fff;border:1px solid #d2d2d2;border-radius:2px;box-shadow:0 2px 4px rgba(0,0,0,.12)}.layui-colorpicker-main-wrapper{height:180px;position:relative}.layui-colorpicker-basis{width:260px;height:100%;position:relative}.layui-colorpicker-basis-white{width:100%;height:100%;position:absolute;top:0;left:0;background:linear-gradient(90deg,#fff,hsla(0,0%,100%,0))}.layui-colorpicker-basis-black{width:100%;height:100%;position:absolute;top:0;left:0;background:linear-gradient(0deg,#000,transparent)}.layui-colorpicker-basis-cursor{width:10px;height:10px;border:1px solid #fff;border-radius:50%;position:absolute;top:-3px;right:-3px;cursor:pointer}.layui-colorpicker-side{position:absolute;top:0;right:0;width:12px;height:100%;background:linear-gradient(red,#ff0,#0f0,#0ff,#00f,#f0f,red)}.layui-colorpicker-side-slider{width:100%;height:5px;box-shadow:0 0 1px #888;box-sizing:border-box;background:#fff;border-radius:1px;border:1px solid #f0f0f0;cursor:pointer;position:absolute;left:0}.layui-colorpicker-main-alpha{display:none;height:12px;margin-top:7px;background:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAIAAADZF8uwAAAAGUlEQVQYV2M4gwH+YwCGIasIUwhT25BVBADtzYNYrHvv4gAAAABJRU5ErkJggg==)}.layui-colorpicker-alpha-bgcolor{height:100%;position:relative}.layui-colorpicker-alpha-slider{width:5px;height:100%;box-shadow:0 0 1px #888;box-sizing:border-box;background:#fff;border-radius:1px;border:1px solid #f0f0f0;cursor:pointer;position:absolute;top:0}.layui-colorpicker-main-pre{padding-top:7px;font-size:0}.layui-colorpicker-pre{width:20px;height:20px;border-radius:2px;display:inline-block;margin-left:6px;margin-bottom:7px;cursor:pointer}.layui-colorpicker-pre:nth-child(11n+1){margin-left:0}.layui-colorpicker-pre-isalpha{background:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAIAAADZF8uwAAAAGUlEQVQYV2M4gwH+YwCGIasIUwhT25BVBADtzYNYrHvv4gAAAABJRU5ErkJggg==)}.layui-colorpicker-pre.layui-this{box-shadow:0 0 3px 2px rgba(0,0,0,.15)}.layui-colorpicker-pre>div{height:100%;border-radius:2px}.layui-colorpicker-main-input{text-align:right;padding-top:7px}.layui-colorpicker-main-input .layui-btn-container .layui-btn{margin:0 0 0 10px}.layui-colorpicker-main-input div.layui-inline{float:left;margin-right:10px;font-size:14px}.layui-colorpicker-main-input input.layui-input{width:150px;height:30px;color:#5f5f5f}.layui-slider{height:4px;background:#eee;border-radius:3px;position:relative;cursor:pointer}.layui-slider-bar{border-radius:3px;position:absolute;height:100%}.layui-slider-step{position:absolute;top:0;width:4px;height:4px;border-radius:50%;background:#fff;-webkit-transform:translateX(-50%);transform:translateX(-50%)}.layui-slider-wrap{width:36px;height:36px;position:absolute;top:-16px;-webkit-transform:translateX(-50%);transform:translateX(-50%);z-index:10;text-align:center}.layui-slider-wrap-btn{width:12px;height:12px;border-radius:50%;background:#fff;display:inline-block;vertical-align:middle;cursor:pointer;transition:.3s}.layui-slider-wrap:after{content:"";height:100%;display:inline-block;vertical-align:middle}.layui-slider-wrap-btn.layui-slider-hover,.layui-slider-wrap-btn:hover{transform:scale(1.2)}.layui-slider-wrap-btn.layui-disabled:hover{transform:scale(1)!important}.layui-slider-tips{position:absolute;top:-42px;z-index:77777777;white-space:nowrap;display:none;-webkit-transform:translateX(-50%);transform:translateX(-50%);color:#fff;background:#000;border-radius:3px;height:25px;line-height:25px;padding:0 10px}.layui-slider-tips:after{content:"";position:absolute;bottom:-12px;left:50%;margin-left:-6px;width:0;height:0;border-width:6px;border-style:solid;border-color:#000 transparent transparent transparent}.layui-slider-input{width:70px;height:32px;border:1px solid #eee;border-radius:3px;font-size:16px;line-height:32px;position:absolute;right:0;top:-14px;box-sizing:border-box}.layui-slider-input-btn{position:absolute;top:0;right:0;width:20px;height:100%;border-left:1px solid #eee}.layui-slider-input-btn i{cursor:pointer;position:absolute;right:0;bottom:0;width:20px;height:50%;font-size:12px;line-height:16px;text-align:center;color:#999}.layui-slider-input-btn i:first-child{top:0;border-bottom:1px solid #eee}.layui-slider-input-txt{height:100%;font-size:14px}.layui-slider-input-txt input{height:100%;border:none;padding-right:21px}.layui-slider-input-btn i:hover{color:#16baaa}.layui-slider-vertical{width:4px;margin-left:33px}.layui-slider-vertical .layui-slider-bar{width:4px}.layui-slider-vertical .layui-slider-step{top:auto;left:0;-webkit-transform:translateY(50%);transform:translateY(50%)}.layui-slider-vertical .layui-slider-wrap{top:auto;left:-16px;-webkit-transform:translateY(50%);transform:translateY(50%)}.layui-slider-vertical .layui-slider-tips{top:auto;left:2px}@media \0screen{.layui-slider-wrap-btn{margin-left:-20px}.layui-slider-vertical .layui-slider-wrap-btn{margin-left:0;margin-bottom:-20px}.layui-slider-vertical .layui-slider-tips{margin-left:-8px}.layui-slider>span{margin-left:8px}}.layui-tree{line-height:22px}.layui-tree .layui-form-checkbox{margin:0!important}.layui-tree-set{width:100%;position:relative}.layui-tree-pack{display:none;padding-left:20px;position:relative}.layui-tree-line .layui-tree-pack{padding-left:27px}.layui-tree-line .layui-tree-set .layui-tree-set:after{content:"";position:absolute;top:14px;left:-9px;width:17px;height:0;border-top:1px dotted #c0c4cc}.layui-tree-entry{position:relative;padding:3px 0;height:26px;white-space:nowrap}.layui-tree-entry:hover{background-color:#eee}.layui-tree-line .layui-tree-entry:hover{background-color:rgba(0,0,0,0)}.layui-tree-line .layui-tree-entry:hover .layui-tree-txt{color:#999;text-decoration:underline;transition:.3s}.layui-tree-main{display:inline-block;vertical-align:middle;cursor:pointer;padding-right:10px}.layui-tree-line .layui-tree-set:before{content:"";position:absolute;top:0;left:-9px;width:0;height:100%;border-left:1px dotted #c0c4cc}.layui-tree-line .layui-tree-set.layui-tree-setLineShort:before{height:13px}.layui-tree-line .layui-tree-set.layui-tree-setHide:before{height:0}.layui-tree-iconClick{display:inline-block;vertical-align:middle;position:relative;height:20px;line-height:20px;margin:0 10px;color:#c0c4cc}.layui-tree-icon{height:14px;line-height:12px;width:14px;text-align:center;border:1px solid #c0c4cc}.layui-tree-iconClick .layui-icon{font-size:18px}.layui-tree-icon .layui-icon{font-size:12px;color:#5f5f5f}.layui-tree-iconArrow{padding:0 5px}.layui-tree-iconArrow:after{content:"";position:absolute;left:4px;top:3px;z-index:100;width:0;height:0;border-width:5px;border-style:solid;border-color:transparent transparent transparent #c0c4cc;transition:.5s}.layui-tree-spread>.layui-tree-entry .layui-tree-iconClick>.layui-tree-iconArrow:after{transform:rotate(90deg) translate(3px,4px)}.layui-tree-txt{display:inline-block;vertical-align:middle;color:#555}.layui-tree-search{margin-bottom:15px;color:#5f5f5f}.layui-tree-btnGroup{visibility:hidden;display:inline-block;vertical-align:middle;position:relative}.layui-tree-btnGroup .layui-icon{display:inline-block;vertical-align:middle;padding:0 2px;cursor:pointer}.layui-tree-btnGroup .layui-icon:hover{color:#999;transition:.3s}.layui-tree-entry:hover .layui-tree-btnGroup{visibility:visible}.layui-tree-editInput{position:relative;display:inline-block;vertical-align:middle;height:20px;line-height:20px;padding:0;border:none;background-color:rgba(0,0,0,.05)}.layui-tree-emptyText{text-align:center;color:#999}.layui-anim{-webkit-animation-duration:.3s;-webkit-animation-fill-mode:both;animation-duration:.3s;animation-fill-mode:both}.layui-anim.layui-icon{display:inline-block}.layui-anim-loop{-webkit-animation-iteration-count:infinite;animation-iteration-count:infinite}.layui-trans,.layui-trans a{transition:all .2s;-webkit-transition:all .2s}@-webkit-keyframes layui-rotate{from{-webkit-transform:rotate(0)}to{-webkit-transform:rotate(360deg)}}@keyframes layui-rotate{from{transform:rotate(0)}to{transform:rotate(360deg)}}.layui-anim-rotate{-webkit-animation-name:layui-rotate;animation-name:layui-rotate;-webkit-animation-duration:1s;animation-duration:1s;-webkit-animation-timing-function:linear;animation-timing-function:linear}@-webkit-keyframes layui-up{from{-webkit-transform:translate3d(0,100%,0);opacity:.3}to{-webkit-transform:translate3d(0,0,0);opacity:1}}@keyframes layui-up{from{transform:translate3d(0,100%,0);opacity:.3}to{transform:translate3d(0,0,0);opacity:1}}.layui-anim-up{-webkit-animation-name:layui-up;animation-name:layui-up}@-webkit-keyframes layui-upbit{from{-webkit-transform:translate3d(0,15px,0);opacity:.3}to{-webkit-transform:translate3d(0,0,0);opacity:1}}@keyframes layui-upbit{from{transform:translate3d(0,15px,0);opacity:.3}to{transform:translate3d(0,0,0);opacity:1}}.layui-anim-upbit{-webkit-animation-name:layui-upbit;animation-name:layui-upbit}@keyframes layui-down{0%{opacity:.3;transform:translate3d(0,-100%,0)}100%{opacity:1;transform:translate3d(0,0,0)}}.layui-anim-down{animation-name:layui-down}@keyframes layui-downbit{0%{opacity:.3;transform:translate3d(0,-5px,0)}100%{opacity:1;transform:translate3d(0,0,0)}}.layui-anim-downbit{animation-name:layui-downbit}@-webkit-keyframes layui-scale{0%{opacity:.3;-webkit-transform:scale(.5)}100%{opacity:1;-webkit-transform:scale(1)}}@keyframes layui-scale{0%{opacity:.3;-ms-transform:scale(.5);transform:scale(.5)}100%{opacity:1;-ms-transform:scale(1);transform:scale(1)}}.layui-anim-scale{-webkit-animation-name:layui-scale;animation-name:layui-scale}@-webkit-keyframes layui-scale-spring{0%{opacity:.5;-webkit-transform:scale(.5)}80%{opacity:.8;-webkit-transform:scale(1.1)}100%{opacity:1;-webkit-transform:scale(1)}}@keyframes layui-scale-spring{0%{opacity:.5;transform:scale(.5)}80%{opacity:.8;transform:scale(1.1)}100%{opacity:1;transform:scale(1)}}.layui-anim-scaleSpring{-webkit-animation-name:layui-scale-spring;animation-name:layui-scale-spring}@keyframes layui-scalesmall{0%{opacity:.3;transform:scale(1.5)}100%{opacity:1;transform:scale(1)}}.layui-anim-scalesmall{animation-name:layui-scalesmall}@keyframes layui-scalesmall-spring{0%{opacity:.3;transform:scale(1.5)}80%{opacity:.8;transform:scale(.9)}100%{opacity:1;transform:scale(1)}}.layui-anim-scalesmall-spring{animation-name:layui-scalesmall-spring}@-webkit-keyframes layui-fadein{0%{opacity:0}100%{opacity:1}}@keyframes layui-fadein{0%{opacity:0}100%{opacity:1}}.layui-anim-fadein{-webkit-animation-name:layui-fadein;animation-name:layui-fadein}@-webkit-keyframes layui-fadeout{0%{opacity:1}100%{opacity:0}}@keyframes layui-fadeout{0%{opacity:1}100%{opacity:0}}.layui-anim-fadeout{-webkit-animation-name:layui-fadeout;animation-name:layui-fadeout}html #layuicss-skincodecss{display:none;position:absolute;width:1989px}.layui-code-wrap{font-size:13px;font-family:"Courier New",Consolas,"Lucida Console"}.layui-code-view{display:block;position:relative;padding:0!important;border:1px solid #eee;border-left-width:6px;background-color:#fff;color:#333}.layui-code-view pre{margin:0!important}.layui-code-header{position:relative;z-index:3;padding:0 11px;height:40px;line-height:40px;border-bottom:1px solid #eee;background-color:#fafafa;font-size:12px}.layui-code-header>.layui-code-header-about{position:absolute;right:11px;top:0;color:#b7b7b7}.layui-code-header-about>a{padding-left:10px}.layui-code-wrap{position:relative;display:block;z-index:1;margin:0!important;padding:11px 0!important;overflow-x:hidden;overflow-y:auto}.layui-code-line{position:relative;line-height:19px;margin:0!important}.layui-code-line-number{position:absolute;left:0;top:0;padding:0 8px;min-width:45px;height:100%;text-align:right;user-select:none;white-space:nowrap;overflow:hidden}.layui-code-line-content{padding:0 11px;word-wrap:break-word;white-space:pre-wrap}.layui-code-ln-mode>.layui-code-wrap>.layui-code-line{padding-left:45px}.layui-code-ln-side{position:absolute;left:0;top:0;bottom:0;z-index:0;width:45px;border-right:1px solid #eee;border-color:rgb(126 122 122 / 15%);background-color:#fafafa;pointer-events:none}.layui-code-nowrap>.layui-code-wrap{overflow:auto}.layui-code-nowrap>.layui-code-wrap>.layui-code-line>.layui-code-line-content{white-space:pre;word-wrap:normal}.layui-code-nowrap>.layui-code-ln-side{border-right-width:0!important;background:0 0!important}.layui-code-fixbar{position:absolute;top:8px;right:11px;padding-right:45px;z-index:5}.layui-code-fixbar>span{position:absolute;right:0;top:0;padding:0 8px;color:#777;transition:all .3s}.layui-code-fixbar>span:hover{color:#16b777}.layui-code-copy{display:none;cursor:pointer}.layui-code-preview>.layui-code-view>.layui-code-fixbar .layui-code-copy{display:none!important}.layui-code-view:hover>.layui-code-fixbar .layui-code-copy{display:block}.layui-code-view:hover>.layui-code-fixbar .layui-code-lang-marker{display:none}.layui-code-theme-dark,.layui-code-theme-dark>.layui-code-header{border-color:rgb(126 122 122 / 15%);background-color:#1f1f1f}.layui-code-theme-dark{border-width:1px;color:#ccc}.layui-code-theme-dark>.layui-code-ln-side{border-right-color:#2a2a2a;background:0 0;color:#6e7681}.layui-code textarea{display:none}.layui-code-preview>.layui-code,.layui-code-preview>.layui-code-view{margin:0}.layui-code-preview>.layui-tab{position:relative;z-index:1;margin-bottom:0}.layui-code-preview>.layui-tab>.layui-tab-title{border-width:0}.layui-code-preview .layui-code-item{display:none}.layui-code-item-preview{position:relative;padding:16px}.layui-code-item-preview>iframe{position:absolute;top:0;left:0;width:100%;height:100%;border:none}.layui-code-tools{position:absolute;right:11px;top:8px;line-height:normal}.layui-code-tools>i{display:inline-block;margin-left:6px;padding:3px;cursor:pointer}.layui-code-tools>i.layui-icon-file-b{color:#999}.layui-code-tools>i:hover{color:#16b777}.layui-code-full{position:fixed;left:0;top:0;z-index:1111111;width:100%;height:100%;background-color:#fff}.layui-code-full .layui-code-item{width:100%!important;border-width:0!important;border-top-width:1px!important}.layui-code-full .layui-code-item,.layui-code-full .layui-code-view,.layui-code-full .layui-code-wrap{height:calc(100vh - 51px)!important;box-sizing:border-box}.layui-code-full .layui-code-item-preview{overflow:auto}.layui-code-view.layui-code-hl{line-height:20px!important;border-left-width:1px}.layui-code-view.layui-code-hl>.layui-code-ln-side{background-color:transparent}.layui-code-theme-dark.layui-code-hl,.layui-code-theme-dark.layui-code-hl>.layui-code-ln-side{border-color:rgb(126 122 122 / 15%)}html #layuicss-laydate{display:none;position:absolute;width:1989px}.layui-laydate *{margin:0;padding:0}.layui-laydate,.layui-laydate *{box-sizing:border-box}.layui-laydate{position:absolute;z-index:99999999;margin:5px 0;border-radius:2px;font-size:14px;line-height:normal;-webkit-animation-duration:.2s;animation-duration:.2s;-webkit-animation-fill-mode:both;animation-fill-mode:both}.layui-laydate-main{width:272px}.layui-laydate-content td,.layui-laydate-header *,.layui-laydate-list li{transition-duration:.3s;-webkit-transition-duration:.3s}.layui-laydate-shade{top:0;left:0;width:100%;height:100%;_height:expression(document.body.offsetHeight+"px");position:fixed;_position:absolute;pointer-events:auto}@keyframes laydate-downbit{0%{opacity:.3;transform:translate3d(0,-5px,0)}100%{opacity:1;transform:translate3d(0,0,0)}}.layui-laydate{animation-name:laydate-downbit}.layui-laydate-static{position:relative;z-index:0;display:inline-block;margin:0;-webkit-animation:none;animation:none}.laydate-ym-show .laydate-next-m,.laydate-ym-show .laydate-prev-m{display:none!important}.laydate-ym-show .laydate-next-y,.laydate-ym-show .laydate-prev-y{display:inline-block!important}.laydate-ym-show .laydate-set-ym span[lay-type=month]{display:none!important}.laydate-time-show .laydate-set-ym span[lay-type=month],.laydate-time-show .laydate-set-ym span[lay-type=year],.laydate-time-show .layui-laydate-header .layui-icon{display:none!important}.layui-laydate-header{position:relative;line-height:30px;padding:10px 70px 5px}.layui-laydate-header *{display:inline-block;vertical-align:bottom}.layui-laydate-header i{position:absolute;top:10px;padding:0 5px;color:#999;font-size:18px;cursor:pointer}.layui-laydate-header i.laydate-prev-y{left:15px}.layui-laydate-header i.laydate-prev-m{left:45px}.layui-laydate-header i.laydate-next-y{right:15px}.layui-laydate-header i.laydate-next-m{right:45px}.laydate-set-ym{width:100%;text-align:center;box-sizing:border-box;text-overflow:ellipsis;overflow:hidden;white-space:nowrap}.laydate-set-ym span{padding:0 10px;cursor:pointer}.laydate-time-text{cursor:default!important}.layui-laydate-content{position:relative;padding:10px;-moz-user-select:none;-webkit-user-select:none;-ms-user-select:none}.layui-laydate-content table{border-collapse:collapse;border-spacing:0}.layui-laydate-content td,.layui-laydate-content th{width:36px;height:30px;padding:0;text-align:center}.layui-laydate-content th{font-weight:400}.layui-laydate-content td{position:relative;cursor:pointer}.laydate-day-mark{position:absolute;left:0;top:0;width:100%;line-height:30px;font-size:12px;overflow:hidden}.laydate-day-mark::after{position:absolute;content:'';right:2px;top:2px;width:5px;height:5px;border-radius:50%}.laydate-day-holidays:before{position:absolute;left:0;top:0;font-size:12px;transform:scale(.7)}.laydate-day-holidays:before{content:'\4F11';color:#ff5722}.laydate-day-holidays[type=work]:before{content:'\73ED';color:inherit}.layui-laydate .layui-this .laydate-day-holidays:before{color:#fff}.layui-laydate-footer{position:relative;height:46px;line-height:26px;padding:10px}.layui-laydate-footer span{display:inline-block;vertical-align:top;height:26px;line-height:24px;padding:0 10px;border:1px solid #c9c9c9;border-radius:2px;background-color:#fff;font-size:12px;cursor:pointer;white-space:nowrap;transition:all .3s}.layui-laydate-footer span:hover{color:#16b777}.layui-laydate-footer span.layui-laydate-preview{cursor:default;border-color:transparent!important}.layui-laydate-footer span.layui-laydate-preview:hover{color:#777}.layui-laydate-footer span:first-child.layui-laydate-preview{padding-left:0}.laydate-footer-btns{position:absolute;right:10px;top:10px}.laydate-footer-btns span{margin:0 0 0 -1px;border-radius:0}.laydate-footer-btns span:first-child{border-radius:2px 0 0 2px}.laydate-footer-btns span:last-child{border-radius:0 2px 2px 0}.layui-laydate-shortcut{width:80px;padding:6px 0;display:inline-block;vertical-align:top;overflow:auto;max-height:276px;text-align:center}.layui-laydate-shortcut+.layui-laydate-main{display:inline-block;border-left:1px solid #e2e2e2}.layui-laydate-shortcut>li{padding:5px 8px;cursor:pointer;line-height:18px}.layui-laydate .layui-laydate-list{position:absolute;left:0;top:0;width:100%;height:100%;padding:10px;box-sizing:border-box;background-color:#fff}.layui-laydate .layui-laydate-list>li{position:relative;display:inline-block;width:33.3%;height:36px;line-height:36px;margin:3px 0;vertical-align:middle;text-align:center;cursor:pointer;list-style:none}.layui-laydate .laydate-month-list>li{width:25%;margin:17px 0}.layui-laydate .laydate-time-list>li{height:100%;margin:0;line-height:normal;cursor:default}.layui-laydate .laydate-time-list p{position:relative;top:-4px;margin:0;line-height:29px}.layui-laydate .laydate-time-list ol{height:181px;overflow:hidden}.layui-laydate .laydate-time-list>li:hover ol{overflow-y:auto}.layui-laydate .laydate-time-list ol li{width:130%;padding-left:33px;height:30px;line-height:30px;text-align:left;cursor:pointer}.layui-laydate-hint{position:absolute;top:115px;left:50%;width:250px;margin-left:-125px;line-height:20px;padding:15px;text-align:center;font-size:12px;color:#ff5722}.layui-laydate-range{width:546px}.layui-laydate-range .layui-laydate-main{display:inline-block;vertical-align:middle;max-width:50%}.layui-laydate-range .laydate-main-list-1 .layui-laydate-content,.layui-laydate-range .laydate-main-list-1 .layui-laydate-header{border-left:1px solid #e2e2e2}.layui-laydate-range.layui-laydate-linkage .laydate-main-list-0 .laydate-next-m,.layui-laydate-range.layui-laydate-linkage .laydate-main-list-0 .laydate-next-y,.layui-laydate-range.layui-laydate-linkage .laydate-main-list-1 .laydate-prev-m,.layui-laydate-range.layui-laydate-linkage .laydate-main-list-1 .laydate-prev-y{display:none}.layui-laydate,.layui-laydate-hint{border:1px solid #d2d2d2;box-shadow:0 2px 4px rgba(0,0,0,.12);background-color:#fff;color:#777}.layui-laydate-header{border-bottom:1px solid #e2e2e2}.layui-laydate-header i:hover,.layui-laydate-header span:hover{color:#16b777}.layui-laydate-content{border-top:none 0;border-bottom:none 0}.layui-laydate-content th{color:#333}.layui-laydate-content td{color:#777}.layui-laydate-content td.laydate-day-now{color:#16b777}.layui-laydate-content td.laydate-day-now:after{content:'';position:absolute;width:100%;height:30px;left:0;top:0;border:1px solid #16b777;box-sizing:border-box}.layui-laydate-linkage .layui-laydate-content td.laydate-selected>div{background-color:#00f7de}.layui-laydate-linkage .laydate-selected:hover>div{background-color:#00f7de!important}.layui-laydate-content td.laydate-selected:after,.layui-laydate-content td:hover:after{content:none}.layui-laydate-content td>div:hover,.layui-laydate-list li:hover,.layui-laydate-shortcut>li:hover{background-color:#eee;color:#333}.laydate-time-list li ol{margin:0;padding:0;border:1px solid #e2e2e2;border-left-width:0}.laydate-time-list li:first-child ol{border-left-width:1px}.laydate-time-list>li:hover{background:0 0}.layui-laydate-content .laydate-day-next,.layui-laydate-content .laydate-day-prev{color:#d2d2d2}.layui-laydate-linkage .laydate-selected.laydate-day-next>div,.layui-laydate-linkage .laydate-selected.laydate-day-prev>div{background-color:#f8f8f8!important}.layui-laydate-footer{border-top:1px solid #e2e2e2}.layui-laydate-hint{color:#ff5722}.laydate-day-mark::after{background-color:#16b777}.layui-laydate-content td.layui-this .laydate-day-mark::after{display:none}.layui-laydate-footer span[lay-type=date]{color:#16b777}.layui-laydate .layui-this,.layui-laydate .layui-this>div{background-color:#16baaa!important;color:#fff!important}.layui-laydate .laydate-disabled,.layui-laydate .laydate-disabled:hover{background:0 0!important;color:#d2d2d2!important;cursor:not-allowed!important;-moz-user-select:none;-webkit-user-select:none;-ms-user-select:none}.layui-laydate-content td>div{padding:7px 0;height:100%}.laydate-theme-molv{border:none}.laydate-theme-molv.layui-laydate-range{width:548px}.laydate-theme-molv .layui-laydate-main{width:274px}.laydate-theme-molv .layui-laydate-header{border:none;background-color:#16baaa}.laydate-theme-molv .layui-laydate-header i,.laydate-theme-molv .layui-laydate-header span{color:#f6f6f6}.laydate-theme-molv .layui-laydate-header i:hover,.laydate-theme-molv .layui-laydate-header span:hover{color:#fff}.laydate-theme-molv .layui-laydate-content{border:1px solid #e2e2e2;border-top:none;border-bottom:none}.laydate-theme-molv .laydate-main-list-1 .layui-laydate-content{border-left:none}.laydate-theme-molv .layui-laydate-footer{border:1px solid #e2e2e2}.laydate-theme-grid .laydate-month-list>li,.laydate-theme-grid .laydate-year-list>li,.laydate-theme-grid .layui-laydate-content td,.laydate-theme-grid .layui-laydate-content thead{border:1px solid #e2e2e2}.layui-laydate-linkage.laydate-theme-grid .laydate-selected,.layui-laydate-linkage.laydate-theme-grid .laydate-selected:hover{background-color:#f2f2f2!important;color:#16baaa!important}.layui-laydate-linkage.laydate-theme-grid .laydate-selected.laydate-day-next,.layui-laydate-linkage.laydate-theme-grid .laydate-selected.laydate-day-prev{color:#d2d2d2!important}.laydate-theme-grid .laydate-month-list,.laydate-theme-grid .laydate-year-list{margin:1px 0 0 1px}.laydate-theme-grid .laydate-month-list>li,.laydate-theme-grid .laydate-year-list>li{margin:0 -1px -1px 0}.laydate-theme-grid .laydate-year-list>li{height:43px;line-height:43px}.laydate-theme-grid .laydate-month-list>li{height:71px;line-height:71px}.laydate-theme-grid .layui-laydate-content td>div{height:29px;margin-top:-1px}.laydate-theme-circle .layui-laydate-content td.layui-this>div,.laydate-theme-circle .layui-laydate-content td>div{width:28px;height:28px;line-height:28px;border-radius:14px;margin:0 4px;padding:0}.layui-laydate.laydate-theme-circle .layui-laydate-content table td.layui-this{background-color:transparent!important}.laydate-theme-grid.laydate-theme-circle .layui-laydate-content td>div{margin:0 3.5px}.laydate-theme-fullpanel .layui-laydate-main{width:526px}.laydate-theme-fullpanel .layui-laydate-list{width:252px;left:272px}.laydate-theme-fullpanel .laydate-set-ym span{display:none}.laydate-theme-fullpanel .laydate-time-show .laydate-set-ym span[lay-type=month],.laydate-theme-fullpanel .laydate-time-show .laydate-set-ym span[lay-type=year],.laydate-theme-fullpanel .laydate-time-show .layui-laydate-header .layui-icon{display:inline-block!important}.laydate-theme-fullpanel .laydate-btns-time{display:none}html #layuicss-layer{display:none;position:absolute;width:1989px}.layui-layer,.layui-layer-shade{position:fixed;_position:absolute;pointer-events:auto}.layui-layer-shade{top:0;left:0;width:100%;height:100%;_height:expression(document.body.offsetHeight+"px")}.layui-layer{-webkit-overflow-scrolling:touch}.layui-layer{top:150px;left:0;margin:0;padding:0;background-color:#fff;-webkit-background-clip:content;border-radius:2px;box-shadow:1px 1px 50px rgba(0,0,0,.3)}.layui-layer-close{position:absolute}.layui-layer-content{position:relative}.layui-layer-border{border:1px solid #b2b2b2;border:1px solid rgba(0,0,0,.1);box-shadow:1px 1px 5px rgba(0,0,0,.2)}.layui-layer-btn a,.layui-layer-setwin span{display:inline-block;vertical-align:middle;*display:inline;*zoom:1}.layui-layer-move{display:none;position:fixed;*position:absolute;left:0;top:0;width:100%;height:100%;cursor:move;opacity:0;filter:alpha(opacity=0);background-color:#fff;z-index:2147483647}.layui-layer-resize{position:absolute;width:15px;height:15px;right:0;bottom:0;cursor:se-resize}.layer-anim{-webkit-animation-fill-mode:both;animation-fill-mode:both;-webkit-animation-duration:.3s;animation-duration:.3s}@-webkit-keyframes layer-bounceIn{0%{opacity:0;-webkit-transform:scale(.5);transform:scale(.5)}100%{opacity:1;-webkit-transform:scale(1);transform:scale(1)}}@keyframes layer-bounceIn{0%{opacity:0;-webkit-transform:scale(.5);-ms-transform:scale(.5);transform:scale(.5)}100%{opacity:1;-webkit-transform:scale(1);-ms-transform:scale(1);transform:scale(1)}}.layer-anim-00{-webkit-animation-name:layer-bounceIn;animation-name:layer-bounceIn}@-webkit-keyframes layer-zoomInDown{0%{opacity:0;-webkit-transform:scale(.1) translateY(-2000px);transform:scale(.1) translateY(-2000px);-webkit-animation-timing-function:ease-in-out;animation-timing-function:ease-in-out}60%{opacity:1;-webkit-transform:scale(.475) translateY(60px);transform:scale(.475) translateY(60px);-webkit-animation-timing-function:ease-out;animation-timing-function:ease-out}}@keyframes layer-zoomInDown{0%{opacity:0;-webkit-transform:scale(.1) translateY(-2000px);-ms-transform:scale(.1) translateY(-2000px);transform:scale(.1) translateY(-2000px);-webkit-animation-timing-function:ease-in-out;animation-timing-function:ease-in-out}60%{opacity:1;-webkit-transform:scale(.475) translateY(60px);-ms-transform:scale(.475) translateY(60px);transform:scale(.475) translateY(60px);-webkit-animation-timing-function:ease-out;animation-timing-function:ease-out}}.layer-anim-01{-webkit-animation-name:layer-zoomInDown;animation-name:layer-zoomInDown}@-webkit-keyframes layer-fadeInUpBig{0%{opacity:0;-webkit-transform:translateY(2000px);transform:translateY(2000px)}100%{opacity:1;-webkit-transform:translateY(0);transform:translateY(0)}}@keyframes layer-fadeInUpBig{0%{opacity:0;-webkit-transform:translateY(2000px);-ms-transform:translateY(2000px);transform:translateY(2000px)}100%{opacity:1;-webkit-transform:translateY(0);-ms-transform:translateY(0);transform:translateY(0)}}.layer-anim-02{-webkit-animation-name:layer-fadeInUpBig;animation-name:layer-fadeInUpBig}@-webkit-keyframes layer-zoomInLeft{0%{opacity:0;-webkit-transform:scale(.1) translateX(-2000px);transform:scale(.1) translateX(-2000px);-webkit-animation-timing-function:ease-in-out;animation-timing-function:ease-in-out}60%{opacity:1;-webkit-transform:scale(.475) translateX(48px);transform:scale(.475) translateX(48px);-webkit-animation-timing-function:ease-out;animation-timing-function:ease-out}}@keyframes layer-zoomInLeft{0%{opacity:0;-webkit-transform:scale(.1) translateX(-2000px);-ms-transform:scale(.1) translateX(-2000px);transform:scale(.1) translateX(-2000px);-webkit-animation-timing-function:ease-in-out;animation-timing-function:ease-in-out}60%{opacity:1;-webkit-transform:scale(.475) translateX(48px);-ms-transform:scale(.475) translateX(48px);transform:scale(.475) translateX(48px);-webkit-animation-timing-function:ease-out;animation-timing-function:ease-out}}.layer-anim-03{-webkit-animation-name:layer-zoomInLeft;animation-name:layer-zoomInLeft}@-webkit-keyframes layer-rollIn{0%{opacity:0;-webkit-transform:translateX(-100%) rotate(-120deg);transform:translateX(-100%) rotate(-120deg)}100%{opacity:1;-webkit-transform:translateX(0) rotate(0);transform:translateX(0) rotate(0)}}@keyframes layer-rollIn{0%{opacity:0;-webkit-transform:translateX(-100%) rotate(-120deg);-ms-transform:translateX(-100%) rotate(-120deg);transform:translateX(-100%) rotate(-120deg)}100%{opacity:1;-webkit-transform:translateX(0) rotate(0);-ms-transform:translateX(0) rotate(0);transform:translateX(0) rotate(0)}}.layer-anim-04{-webkit-animation-name:layer-rollIn;animation-name:layer-rollIn}@keyframes layer-fadeIn{0%{opacity:0}100%{opacity:1}}.layer-anim-05{-webkit-animation-name:layer-fadeIn;animation-name:layer-fadeIn}@-webkit-keyframes layer-shake{0%,100%{-webkit-transform:translateX(0);transform:translateX(0)}10%,30%,50%,70%,90%{-webkit-transform:translateX(-10px);transform:translateX(-10px)}20%,40%,60%,80%{-webkit-transform:translateX(10px);transform:translateX(10px)}}@keyframes layer-shake{0%,100%{-webkit-transform:translateX(0);-ms-transform:translateX(0);transform:translateX(0)}10%,30%,50%,70%,90%{-webkit-transform:translateX(-10px);-ms-transform:translateX(-10px);transform:translateX(-10px)}20%,40%,60%,80%{-webkit-transform:translateX(10px);-ms-transform:translateX(10px);transform:translateX(10px)}}.layer-anim-06{-webkit-animation-name:layer-shake;animation-name:layer-shake}@-webkit-keyframes fadeIn{0%{opacity:0}100%{opacity:1}}@keyframes layer-slide-down{from{transform:translate3d(0,-100%,0)}to{transform:translate3d(0,0,0)}}@keyframes layer-slide-down-out{from{transform:translate3d(0,0,0)}to{transform:translate3d(0,-100%,0)}}.layer-anim-slide-down{animation-name:layer-slide-down}.layer-anim-slide-down-out{animation-name:layer-slide-down-out}@keyframes layer-slide-left{from{transform:translate3d(100%,0,0)}to{transform:translate3d(0,0,0)}}@keyframes layer-slide-left-out{from{transform:translate3d(0,0,0)}to{transform:translate3d(100%,0,0)}}.layer-anim-slide-left{animation-name:layer-slide-left}.layer-anim-slide-left-out{animation-name:layer-slide-left-out}@keyframes layer-slide-up{from{transform:translate3d(0,100%,0)}to{transform:translate3d(0,0,0)}}@keyframes layer-slide-up-out{from{transform:translate3d(0,0,0)}to{transform:translate3d(0,100%,0)}}.layer-anim-slide-up{animation-name:layer-slide-up}.layer-anim-slide-up-out{animation-name:layer-slide-up-out}@keyframes layer-slide-right{from{transform:translate3d(-100%,0,0)}to{transform:translate3d(0,0,0)}}@keyframes layer-slide-right-out{from{transform:translate3d(0,0,0)}to{transform:translate3d(-100%,0,0)}}.layer-anim-slide-right{animation-name:layer-slide-right}.layer-anim-slide-right-out{animation-name:layer-slide-right-out}.layui-layer-title{padding:0 81px 0 16px;height:50px;line-height:50px;border-bottom:1px solid #f0f0f0;font-size:14px;color:#333;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;border-radius:2px 2px 0 0}.layui-layer-setwin{position:absolute;right:15px;*right:0;top:16px;font-size:0;line-height:initial}.layui-layer-setwin span{position:relative;width:16px;height:16px;line-height:18px;margin-left:10px;text-align:center;font-size:16px;cursor:pointer;color:#000;_overflow:hidden;box-sizing:border-box}.layui-layer-setwin .layui-layer-min:before{content:'';position:absolute;width:12px;border-bottom:1px solid #2e2d3c;left:50%;top:50%;margin:-.5px 0 0 -6px;cursor:pointer;_overflow:hidden}.layui-layer-setwin .layui-layer-min:hover:before{background-color:#2d93ca}.layui-layer-setwin .layui-layer-max:after,.layui-layer-setwin .layui-layer-max:before{content:'';position:absolute;left:50%;top:50%;z-index:1;width:9px;height:9px;margin:-5px 0 0 -5px;border:1px solid #2e2d3c}.layui-layer-setwin .layui-layer-max:hover:after,.layui-layer-setwin .layui-layer-max:hover:before{border-color:#2d93ca}.layui-layer-setwin .layui-layer-min:hover:before{background-color:#2d93ca}.layui-layer-setwin .layui-layer-maxmin:after,.layui-layer-setwin .layui-layer-maxmin:before{width:7px;height:7px;margin:-3px 0 0 -3px;background-color:#fff}.layui-layer-setwin .layui-layer-maxmin:after{z-index:0;margin:-5px 0 0 -1px}.layui-layer-setwin .layui-layer-close{cursor:pointer}.layui-layer-setwin .layui-layer-close:hover{opacity:.7}.layui-layer-setwin .layui-layer-close2{position:absolute;right:-28px;top:-28px;color:#fff;background-color:#787878;padding:3px;border:3px solid;width:28px;height:28px;font-size:16px;font-weight:bolder;border-radius:50%;margin-left:0;*right:-18px;_display:none}.layui-layer-setwin .layui-layer-close2:hover{opacity:unset;background-color:#3888f6}.layui-layer-btn{text-align:right;padding:0 15px 12px;pointer-events:auto;user-select:none;-webkit-user-select:none}.layui-layer-btn a{height:30px;line-height:30px;margin:5px 5px 0;padding:0 16px;border:1px solid #dedede;background-color:#fff;color:#333;border-radius:2px;font-weight:400;cursor:pointer;text-decoration:none;box-sizing:border-box}.layui-layer-btn a:hover{opacity:.9;text-decoration:none}.layui-layer-btn a:active{opacity:.8}.layui-layer-btn .layui-layer-btn0{border-color:transparent;background-color:#1e9fff;color:#fff}.layui-layer-btn-l{text-align:left}.layui-layer-btn-c{text-align:center}.layui-layer-dialog{min-width:240px}.layui-layer-dialog .layui-layer-content{position:relative;padding:16px;line-height:24px;word-break:break-all;overflow:hidden;font-size:14px;overflow-x:hidden;overflow-y:auto}.layui-layer-dialog .layui-layer-content .layui-layer-face{position:absolute;top:18px;left:16px;color:#959595;font-size:32px;_left:-40px}.layui-layer-dialog .layui-layer-content .layui-icon-tips{color:#f39b12}.layui-layer-dialog .layui-layer-content .layui-icon-success{color:#16b777}.layui-layer-dialog .layui-layer-content .layui-icon-error{top:19px;color:#ff5722}.layui-layer-dialog .layui-layer-content .layui-icon-question{color:#ffb800}.layui-layer-dialog .layui-layer-content .layui-icon-lock{color:#787878}.layui-layer-dialog .layui-layer-content .layui-icon-face-cry{color:#ff5722}.layui-layer-dialog .layui-layer-content .layui-icon-face-smile{color:#16b777}.layui-layer-rim{border:6px solid #8d8d8d;border:6px solid rgba(0,0,0,.3);border-radius:5px;box-shadow:none}.layui-layer-msg{min-width:180px;border:1px solid #d3d4d3;box-shadow:none}.layui-layer-hui{min-width:100px;background-color:#000;filter:alpha(opacity=60);background-color:rgba(0,0,0,.6);color:#fff;border:none}.layui-layer-hui .layui-layer-close{color:#fff}.layui-layer-hui .layui-layer-content{padding:11px 24px;text-align:center}.layui-layer-dialog .layui-layer-padding{padding:18px 24px 18px 58px;text-align:left}.layui-layer-page .layui-layer-content{position:relative;overflow:auto}.layui-layer-iframe .layui-layer-btn,.layui-layer-page .layui-layer-btn{padding-top:10px}.layui-layer-nobg{background:0 0}.layui-layer-iframe iframe{display:block;width:100%}.layui-layer-loading{border-radius:100%;background:0 0;box-shadow:none;border:none}.layui-layer-loading .layui-layer-content{width:76px;height:38px;line-height:38px;text-align:center}.layui-layer-loading-icon{font-size:38px;color:#959595}.layui-layer-loading2{text-align:center}.layui-layer-loading-2{position:relative;height:38px}.layui-layer-loading-2:after,.layui-layer-loading-2:before{content:'';position:absolute;left:50%;top:50%;width:38px;height:38px;margin:-19px 0 0 -19px;border-radius:50%;border:3px solid #d2d2d2;box-sizing:border-box}.layui-layer-loading-2:after{border-color:transparent;border-left-color:#1e9fff}.layui-layer-tips{background:0 0;box-shadow:none;border:none}.layui-layer-tips .layui-layer-content{position:relative;line-height:22px;min-width:12px;padding:8px 15px;font-size:12px;_float:left;border-radius:2px;box-shadow:1px 1px 3px rgba(0,0,0,.2);background-color:#000;color:#fff}.layui-layer-tips .layui-layer-close{right:-2px;top:-1px}.layui-layer-tips i.layui-layer-TipsG{position:absolute;width:0;height:0;border-width:8px;border-color:transparent;border-style:dashed;*overflow:hidden}.layui-layer-tips i.layui-layer-TipsB,.layui-layer-tips i.layui-layer-TipsT{left:5px;border-right-style:solid;border-right-color:#000}.layui-layer-tips i.layui-layer-TipsT{bottom:-8px}.layui-layer-tips i.layui-layer-TipsB{top:-8px}.layui-layer-tips i.layui-layer-TipsL,.layui-layer-tips i.layui-layer-TipsR{top:5px;border-bottom-style:solid;border-bottom-color:#000}.layui-layer-tips i.layui-layer-TipsR{left:-8px}.layui-layer-tips i.layui-layer-TipsL{right:-8px}.layui-layer-lan .layui-layer-title{background:#4476a7;color:#fff;border:none}.layui-layer-lan .layui-layer-btn{padding:5px 10px 10px;border-top:1px solid #e9e7e7}.layui-layer-lan .layui-layer-btn a{background:#fff;border-color:#e9e7e7;color:#333}.layui-layer-lan .layui-layer-btn .layui-layer-btn1{background:#c9c5c5}.layui-layer-molv .layui-layer-title{background:#009f95;color:#fff;border:none}.layui-layer-molv .layui-layer-btn a{background:#009f95;border-color:#009f95}.layui-layer-molv .layui-layer-btn .layui-layer-btn1{background:#92b8b1}.layui-layer-lan .layui-layer-setwin .layui-icon,.layui-layer-molv .layui-layer-setwin .layui-icon{color:#fff}.layui-layer-win10{border:1px solid #aaa;box-shadow:1px 1px 6px rgba(0,0,0,.3);border-radius:none}.layui-layer-win10 .layui-layer-title{height:32px;line-height:32px;padding-left:8px;border-bottom:none;font-size:12px}.layui-layer-win10 .layui-layer-setwin{right:0;top:0}.layui-layer-win10 .layui-layer-setwin span{margin-left:0;width:32px;height:32px;padding:8px}.layui-layer-win10.layui-layer-page .layui-layer-setwin span{width:38px}.layui-layer-win10 .layui-layer-setwin span:hover{background-color:#e5e5e5}.layui-layer-win10 .layui-layer-setwin span.layui-icon-close:hover{background-color:#e81123;color:#fff}.layui-layer-win10.layui-layer-dialog .layui-layer-content{padding:8px 16px 32px;color:#0033bc}.layui-layer-win10.layui-layer-dialog .layui-layer-padding{padding-top:18px;padding-left:58px}.layui-layer-win10 .layui-layer-btn{padding:5px 5px 10px;border-top:1px solid #dfdfdf;background-color:#f0f0f0}.layui-layer-win10 .layui-layer-btn a{height:20px;line-height:18px;background-color:#e1e1e1;border-color:#adadad;color:#000;font-size:12px;transition:all .3s}.layui-layer-win10 .layui-layer-btn a:hover{border-color:#2a8edd;background-color:#e5f1fb}.layui-layer-win10 .layui-layer-btn .layui-layer-btn0{border-color:#0078d7}.layui-layer-prompt .layui-layer-input{display:block;width:260px;height:36px;margin:0 auto;line-height:30px;padding-left:10px;border:1px solid #e6e6e6;color:#333}.layui-layer-prompt textarea.layui-layer-input{width:300px;height:100px;line-height:20px;padding:6px 10px}.layui-layer-prompt .layui-layer-content{padding:16px}.layui-layer-prompt .layui-layer-btn{padding-top:0}.layui-layer-tab{box-shadow:1px 1px 50px rgba(0,0,0,.4)}.layui-layer-tab .layui-layer-title{padding-left:0;overflow:visible}.layui-layer-tab .layui-layer-title span{position:relative;display:inline-block;vertical-align:top;border-left:1px solid transparent;border-right:1px solid transparent;min-width:80px;max-width:300px;padding:0 16px;text-align:center;cursor:default;text-overflow:ellipsis;overflow:hidden;white-space:nowrap;cursor:pointer}.layui-layer-tab .layui-layer-title span.layui-this{height:51px;border-left-color:#eee;border-right-color:#eee;background-color:#fff;z-index:10}.layui-layer-tab .layui-layer-title span:first-child{border-left-color:transparent}.layui-layer-tabmain{line-height:24px;clear:both}.layui-layer-tabmain .layui-layer-tabli{display:none}.layui-layer-tabmain .layui-layer-tabli.layui-this{display:block}.layui-layer-photos{background:0 0;box-shadow:none}.layui-layer-photos .layui-layer-content{overflow:visible;text-align:center}.layui-layer-photos .layer-layer-photos-main img{position:relative;width:100%;display:inline-block;*display:inline;*zoom:1;vertical-align:top}.layui-layer-photos-next,.layui-layer-photos-prev{position:fixed;top:50%;width:52px;height:52px;line-height:52px;margin-top:-26px;cursor:pointer;font-size:52px;color:#717171}.layui-layer-photos-prev{left:32px}.layui-layer-photos-next{right:32px}.layui-layer-photos-next:hover,.layui-layer-photos-prev:hover{color:#959595}.layui-layer-photos-toolbar{position:fixed;left:0;right:0;bottom:0;width:100%;height:52px;line-height:52px;background-color:#000\9;filter:Alpha(opacity=60);background-color:rgba(0,0,0,.32);color:#fff;text-overflow:ellipsis;overflow:hidden;white-space:nowrap;font-size:0}.layui-layer-photos-toolbar>*{display:inline-block;vertical-align:top;padding:0 16px;font-size:12px;color:#fff;*display:inline;*zoom:1}.layui-layer-photos-toolbar *{font-size:12px}.layui-layer-photos-header{top:0;bottom:auto}.layui-layer-photos-header>span{cursor:pointer}.layui-layer-photos-header>span:hover{background-color:rgba(51,51,51,.32)}.layui-layer-photos-header .layui-icon{font-size:18px}.layui-layer-photos-footer>h3{max-width:65%;text-overflow:ellipsis;overflow:hidden;white-space:nowrap}.layui-layer-photos-footer a:hover{text-decoration:underline}.layui-layer-photos-footer em{font-style:normal}@-webkit-keyframes layer-bounceOut{100%{opacity:0;-webkit-transform:scale(.7);transform:scale(.7)}30%{-webkit-transform:scale(1.05);transform:scale(1.05)}0%{-webkit-transform:scale(1);transform:scale(1)}}@keyframes layer-bounceOut{100%{opacity:0;-webkit-transform:scale(.7);-ms-transform:scale(.7);transform:scale(.7)}30%{-webkit-transform:scale(1.05);-ms-transform:scale(1.05);transform:scale(1.05)}0%{-webkit-transform:scale(1);-ms-transform:scale(1);transform:scale(1)}}.layer-anim-close{-webkit-animation-name:layer-bounceOut;animation-name:layer-bounceOut;-webkit-animation-fill-mode:both;animation-fill-mode:both;-webkit-animation-duration:.2s;animation-duration:.2s}@media screen and (max-width:1100px){.layui-layer-iframe{overflow-y:auto;-webkit-overflow-scrolling:touch}} \ No newline at end of file diff --git a/src/main/resources/scripts/layui/font/iconfont.eot b/src/main/resources/scripts/layui/font/iconfont.eot new file mode 100644 index 0000000000000000000000000000000000000000..3f5e98bb584b80ae0ce0d39b598fb37423ea8220 GIT binary patch literal 54172 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

?x(Azz|<-o<;&g<&wtZdwT(dKbDA!{34 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

N=BJJ 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 literal 0 HcmV?d00001 diff --git a/src/main/resources/scripts/layui/font/iconfont.svg b/src/main/resources/scripts/layui/font/iconfont.svg new file mode 100644 index 0000000..4f1920d --- /dev/null +++ b/src/main/resources/scripts/layui/font/iconfont.svg @@ -0,0 +1,405 @@ + + + + Created by iconfont + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/scripts/layui/font/iconfont.ttf b/src/main/resources/scripts/layui/font/iconfont.ttf new file mode 100644 index 0000000000000000000000000000000000000000..6e84a89e031247509801ae5d65cc8d20323cb766 GIT binary patch literal 53996 zcmd?RcbptYoi|+7H614BypwlkHqGqr%+Ac_oz?DYmGdgB(#pzKux!ae0^722HW&x6 zal{9hU>k!Y28@vf+!1WffDiDIgb!LAKCnr59B5n5x4L&_3Cwf%dG2}sct7vX{;I31 ztE#)wFIN#t2(gl*gprQ%l@oK4H)oF!Lc5W=YU#@M(vF5D$U)zY`>Cz_wj9{{FZXXE z#PoYYY~SCx_wbG%eEEUf3GuEXiaiBn6meRty_L|9e?D05oy=$+j7|fA;f-(cnFEf{af~J?{j^6l#s7JMTl_a zfeQ{Edi=dTe<0)=U4+Qb5_}mudip5YAb#TX^yz7_%u~+!JFcf7^<%&J+WDKE{eK`< zlb+$kWnbWJB>al9SBPcyF5D_a&>!Oya6Qg`Ocv?)Jk^39A*c0w^R-Kd*>F^Z#<#M$DdN9P$JEwnoNw|N<_5vMgcuS-fLXx>!d_c zQXnEj>f8J++cCC{9CL}U7xn%i?1$uPWIMMv1pY!3IeprMbR#}~BQh%Tx@e}{|>f+zUs|4`1)CNEjd(K!Z!ZS!kYmmWk_R)nBQnP6{W*lYx_!lk-llKDqYffs;3!eC*`sPoDUN@{2Y9 z`tUEG|K$s>7f%CoxI~rcPXix*ihPnhMjj!L zk|y#n$&*izkCTs)hsa0BhslS?-;oDMC;1?0B@dDok|H;gcav+$JIH&;E#$rAR&pD; zo!mj*N8V5FBzKVykh{q}4v_1}P2>i0Be|YjLoOxnB1g$pz;lmoJUrZHDoPWK~|C_!0HRg7@1Fo$Xqgy z)X6w0k`dBJ_LFVsr+zX@x=9b|B~?-*U8F*GlXlFBG|7LCQoX337;B45U7XY$OG; zi7X~l#7DN064^sMWFZNXEXk35WB_x~L|j1UoKF1z{m;fbz}zs?4KMIDWdLRgRw}KvYm?4px-yv1F+JF>M;Ooj;LM(u=0Be(|K?AT#i3)y;1J)~1 z!vzb%B1F*u08aDuIou~-|uJ_>QPO1_1LBwbuaPK%({;0BlIqegl9Pi8^2a zFeFh24FIkr>W~4znnbM|0Q^bRVFQ3ki8{vs;8dcH7y#@_)KLR~XNfvy05C35=NbUq zOVn`#$WdJ98324t)cFQ*4pd!W0B|%>7a9O;P1HpO0B;j@u>ru~L|tM4a5+(z8UUM`9ssIC)F}f%e~8M<06>X| z%F6&ilZd+208l5QZZm*OA1ePo04Nqw`S$@pyNJra2>>caRQ^o>&@-a$HUN~3sOK91 z8b{PU29P?gye|Ntdqmx504N|)_Zt9ONK`(b0HBIQ=TKwF8*=Mw-_mZ*G;0id@;eU8-rW2LVX#l7@ zQLixobe^c!8bFe`US|MkKT)qY09*i3Z!iG-08wu=0Gt6)Z!!Qp0#R=^fb*B?yA1%} zKva%{0N@~qdW!+zC5ZZ71He@f^;QGGUl8>+1Hfqz^>zcma}f0o1HgR{^?e3_4}0lXMF zSuuc2;fnRc0sI*`x!M45YUJcv1HiM9lUN@dz`c=^m=7Gl$B~nd832xsocz22;O)rC z69$0GBfn4#0KZ3mvBm&!e&k;tHUK;z`Q`HlaJl7|FBrgOm)DC1a1IfFw^9$Cq|XUC zmShLmv!+JVHKI$rM*Oa{NP0s0qkN0}RmG%y)7)gf!hFhdiPd4oJm&GnSq<-WOVf2te`>z7rK{ycTe|I7;rzmj1+BQOeN+1_9ZJV&$JHIjJB7}VmU>IO zO2^Bg@&n~Bmw(@Ne%E&^AF2GYx~%%cTB)|T_I$U{t@fPP^J4Gb-tYH~_C3+}Z~dnJ zd-_id^bg!L@ae%_gTEeX9NIthOnp=R%fpj%0(17yd2Qs0(a7jW$I@fnW51t!-`wAi zFBrdJ{Lb-@&Yw5`sRfY*8J@)ndDE&ZR{irtVB*e+->;rr z{nQ%Un)B8iS@YwybJpIo_OZ3c*Bw~*-AUWzQv3da-p<@mn~%|ie{B7(%I4K3*vb8>FpW^vmC3S*g$C3A*z`Rcr}#yF+Z*Q}dZU2(S;Z+dvLw5E%ulgzzsc=X;{ zTWd+CA!lvg@|K>y&Xv7!fwoYQ+7uJe4OS=mM_^ZRI!SY_G>xX{Kfe#{N6haFl+Zco zlxhtfQL1*KlXwpFR)r_(zp^CA67Rr#&O!Cw4F<|3{>)ote^P9b-|u*6`=#TH<2J7| z(G|-${m$jJH5+!dugzGUPV2-Ew{*66&2DOrjtuRadSL6X3Lihb<=}!bkK63Gie`Uc z?qZ++qAl-SyR#&)OU))F)G#o-zqNa0pwzlz^{(@ICHK8==jsjBa1?bPnkse2lHG&t z>8b9nNI*9IR{PpWRTdj$t0;Q>qTSpe3)Qg?JV6sSzKXe4hqVH~$2sk#DNKY^TJpPn zfpQA1cX#EosZxTb(sHG%N_`TZ=8#&&(}16SLtA;k70|vPNF)Mm55iMd(k79u(5?_A znH`o)r`~UpWfR+Dl7vSNxRQbCI?pQ91IY=tf-V%L>555`OhR3hB+>XD_D6hA4BR;Q z2wJ4Aw1R$5rQ7u}I`d^UZ#9Q8Nl^ZsffzmzFT{6Irhg(H^K|yQ9JCE50@~Fkw%-}h z^zV#$V&Z<2cxsNOC(Winsxd^?Dy;t_|6N!3tt5u{tn0h4`GCiHu;T4<#`jZ6!et=yn<^! z$5)jbA%hvvW>scP!JT2+ZSY z3@0N4M+YLwaK`Ilg3^_Vb+-;jq;#q?X7f4RW-)FRt$wdXw6`VN{4T0k+*|K$ZOi9w zn%wv;!ETb6=ohFcnE(O7B#WXP69h>yQCZ1TQBedyi)@zgpQEx_Sb?yX*~jc26$u@r7FpSf^emb-P_&vt2B-hJ8JYC%Zc$UOCk2Vy2Scfl8c; zVotf7d6$(@w$Z96EcNcq=j}CGJ6#qt!E`DD)KEkB*C@ujtkW33 zPomR@cWv(<80g==>$kgh^bHL3?bvmA&&G9?T4zycDP8rG>2LMa$L8oNQKWXT+>lpx_tBc``g887 zbJ++h9}`|DA9|PQU&3@M^Ov0noFDBdd8^67`;~}? zDK9D!^R;H?ikSCj-m%-5BfI~7Hu8=OHgBwUcUL!V{_$)?FzdO0XJ*QNy@n0i?U*lb zmC*|~pHoIJjClg?RR8`FV6h5hO>qBNPT0G0X{nYeQXRP<0K-E;-u(d#LS;}WmjXc6 zeDdH2Zb@_qMFIR!tr};N{I+X>z7j|T@SkILkn>zDYnKI^&5`rxyQw$gr7iPXQ-eXD zp9&6lA!2g~9vk)5Zn@{y{eAuW=dF73{vE-dVeO40yLa$avt#$~XCohU1p>)HU@`UC z(|!J2(9xK*Ib5|SyT@a1o!1f#`D^i1BvA;vy%G1#-Tgw}zW3gH%f48nZQWf@JW?0F zb53n~wMM^u0pmKN&)1O|TJ_(YvEEFjC^!V!j~)y9C7oBugHC4$g+S1s5y-iX80_1z z`^bUKSW?}!jhj!*Mp~RgQ+>f;Bej}sl-kU+h^s~WlhCuQk9`do;jBKuSN9FFjpvlt zE7A@v$@Zs(++cRDP5G)~Lm=D6o4SQc6UeXLPE6uggb!l;{g7=S&4J=#y*>C+zDx!|dgG4Wl1JLTW9CwSEj%~;njR3{_R7EU zXhpv2a*Yb-{kjP*wA@SHz@9otg)$&}MPDQP>?qVH6FyQPl0Syx?)E zzE00C+@4xqTamBPer7iP+!1in$D9ENZw7G7G4=u*#u{qma{Sxk1-YxrnLO}vI+Irk z@~OwCAePB2h5Y?&%FVVEZb!H zQ8^rxrB9liZmaT5i^&zSzGO8i7Hy5ijO#a)h~K1q-I7413l@JuufMfrfi>b{mijoa z^7$3ZiXCk&3l?p(d%gCRw^u7Yv?NLqD=k`Od)TIZM={&vdz46kDG06hcgS+oLVXsK zGh)^L&5m*7yu+8-b8Hmjmi!x(+MAd2a!?M|g0(z7b%vGs-RwZs3fJFIxc>UW4c8a0 z@8=_Q=7}B=7S2AzbG%ZQH~)iuk(s&vk3w208k1#)7sm3?DJB;j4S`&Eb?%`=;?T|2 zazn$Mt_>qAT9cAPO17>T+0Zqop`l!5%5bGJ+?eXE4Q$!HYGSZE?q$py?;f03wR_7z ztvA)k-wRRq^mFVOtD~(ca7ul^EECWZUjSV<-Z`(+-&>z~ zA>JxH`&Q0VZ@d+^zV#BJ?XCDc{vBoy|FG#7-ip6|&X>>5h0n7r@J1DLA>+>J5+U#; zeq7gpDZsHNew_Khy5k}d5G0qTW`Dxag6_fIHC9oPZMorrqj{G@pss>9X*Da7jVe@N zK9||XypqLfOL$w;&al(X>eJVUt>#5tp-9kKkfh$SuO;T|q<7ns-c+uk*qCH?CYhXG zOH64?_O>@Q`g3t-Gz=9q=cA6Z=h-0mz!Y@bJ$x2B1YcYz4GLXF!Rr7T2Ztn-;(|}g zLUCZI5rVaI2YKCxuNWD*;=@lEm(O?Ka^Ec%^z~g}T(sw0LC9#QJ@%JvUa#$CyN3h*0N~c#6p)qsi z^46}d)~4dV!os$;g#|huT-9A!9&9k3`a}GZB_kJP6Hrfcz1GX@ORSgMa6~kNWABinbn*wHaS>EFunK3)tIGmQI{n3 zNP$axqjQ)&XX{rayI?CcD09u0NRvMtYVmmEF_S=LtIr;v7|O0&wyzOg&3rA zGoG@NkOaN864R;xw_odRbD;$kL%WyyQtUg%z0_LSurbiS&{PLREAa8*M#z9 zm9!JL$tGXJ#A^+Kg1668njGtaiOqyJr4}N|oLn?1aE+3H;zCkO8` zb+0QG7PYl4D&$9lLOMUEnZ8^5R#Usp?E8Y#afItIwBf!|YwEeirgG<}_I#j8m^c5z zc(Ty8MElRus*Y4)(d44GR9tB5ZkbmU8`hK($v~s_S;{pZM^! z+(l(C=R+_U$AnYhgzKCZ>)4&E-TD0F({x6w;h+pHoM9CpuyDkMA8)AM1(0SqT@Xm~ zUGtWARO|b$9B$3$TZgaQSFd&~pLf@vJ6Cq5{`JdKp$u|~fVaQIW1 zl4)xG%89M3hxgSRys?ch!s&GQ521`zdsifT6~DR42(uA-`N_nEBO@0k7BZO$Y+*3C5Eq$g56l)y zqc043Wii%~3w-Y!-U~Wr11rigA4FkhU^c{cJXaBUCxKW1=V2hZ#u~a;-7qr1357s< zHg|V#zK92F@^hBV$?F0AkvHt6*2M>}Ik?zL5w}kqT?p=N;n7t#O1d}qJg67bFZ4j3 zm&!kATsrJtx2?XbuWwh~=Jwjf1@Blae~2?~ACecnW5LXLzQ#Vw;-GXQY}ZN5xM8%U zCf9;8OXsst0CEFGAI<8^$BmXyx9FyrX%zhG+bG$bTq!RJ|MJwcPs>kVAj{gL58B*r z+kUE1RW*ssLjv#Is+3w9LzMG}eDPzf0zo(UFo?&4LdA{6h8dibxj2`qluKL* z9|zAP$(bs!IOi&Z8O|2ya;ZO4q!q6los-Uq8C-o5Yd~fP^SLAWT7OScR^n}a-N_EC z)#QLmITdP1gj@|NH+{mDYH&Jj{x+AaoF>zrr#qXS9+P6T2d)okUl2Ub=8k;$`lLf* zy-v%2{K;&uz51*rNMHHlPXfYPizQUZm>niDohn$&=}6kdynatx$YSvYGv#oLMfoC z8UtedhA0Mt{}>YTV99hwzs4%GE0Uu5;pUc9KF}PpxC0(bDCLQVhA&FBCS&a_u|mP; zvjrl~a4qEMumu9CSUBQphy|@OrCuKx40=_};WXtZYEHM<($dsGr z!ZimLyQt8y-UU0>O|2anjJ9%vtHsLgbv~0sVXgEvwUr|wsyL##$_TwY-CSrDTI)^4 zX6E-nh@IuTUSM_jX4KIa2FGuh4TCvu4h5N=(ZTEeV%TyxD}j-m?H^t41#=FqJTXM2 z2W%m$)i$;_mgp+4nS6N?JX9jKcg$wBhHNV?PdA=Xh%}}zXLYY#o8lOPK4|x*MN2p& z#?GHRzH}Ea#>?+oIzIROm>3FM#PmCtuiS7(>2hSl%H{9m?EwA4^msw|I(#C^kQSD4 z9~68fP#;${#krE0YX`Xumg8DC&UQG!NaG1cPG=_nt7ly_GplwMzSFw*lD(}=XcT0Y zTD4(ST96T^2S-*;JUg*+RG_R;z^WOa|G@n5UM2~Rj9#?yz@h&0UeZM%N5 zYx_$6Owj7}Y&DC2>dQ7a=TH(uDa4aW*t>;RCcU>ifxm9Ni>G%+qR~jF{zQJyqGrsE zg|GwhC!>woToy0qWwa$xUSn2YKj@oQ;RUQ5?sJ?6pTfrqQ`|pe`^%IK4+7kylf*Oa zWUb|nCoa15>H|Bs-P6+gPZPa8Q`-)1Shl#(ews{_=9chZzxvXHk9U?P+FI_}y7R(A zTeo!gtu7W9FI6(7(s&6TEu3Z{ctCgr?@w?a7n(Ar8PJ`s|6_k)l$OP;KJ`RyTAkrr z=S&~XRVq1whcw`vKFU>p!ck{nmG*}ArX^q#Ei^7G+%lGcsV^WrQq5}Xa#h%vKbEad zYc4-be;?z8s*gS@iQa(pw8`|e6!020>*sYHU>MpJ*hkLMx*03LFpg&2eA*alx3{b# zumeMd!d07GA-I0kvdhz%O#1R=tDbo)x_&Q=WVITsVc(yPd|>*I+-xf-2!}I#UVjFY zIo$}3>r9+2=S5!C4Qw-!nzq*IbK-wS=X?y^%lowzo^IpZOAzIC8$0@Lws*N@+MuY= z5sG-O8?C`Uv9sd;RXJKe1_gRV4}7+xHlQyAl++y_ubl^r&&24#x68^%E;4k}3|5iqw7yO)V%UTStk12TH9p*YPTFvVdSRWq0&kxk(2jaqi z8R9S(4Y{@sR>f)!HV*EPaAwuVfWal68~rsXaJmabVHg+k87Y^qfZ&!PN#!#Y?n#4H z1FQ%Ed{97zq6C9AP)kjBEdh_eoYG=p(bCT>T~sJ`3{g6_(r{#nWT_0~^8*!2&ty2^ z@CgA&M=o0*zI)D4Hs9&+Q|forBv@qaWfPo1Y$+~tfVEbW8s zmPE`xugl~3t6 z#p};hz|L3ntD)zV!4Po?AXuK!b)Ip|;2HJIf8nqpqLQCqOpj}WpZN@Jv-`Y`YaKqH zd|2{GztVMo@7qpv~rPHs^)?xS2 z?~II0zdkZT7td6N8b{{L;r!z1e`1fY0x)tXIL7tx559=|GE>;*sLZ`bV2R6d9}=*N zW$^4AVGhtT=9{}nv$<*zI0ue1Gfq9c%^VB)q1}=*hR+RnOY}y~ZREw;=dxTMtp=og z8M7OG#3#E5Q5*tMDvp}iqIRJ>s~vGCLw4HjNrr4fDc4=e((AI7_FPR6x~D(NWsB=v zQLRY79dKz!VBB=!K5z>SdD<%|o2_)aeDGXw3V-K{0&#N(p}g5*F>h?)VGCS^=trmj z$Qczu!HuxtaIPfSLP^hIYDcJjK3fyIvXx4z&*zbs%6Xr#!R=-J0mQTsr-k-8EVo%4^aLzJzLjEUYpEeA?0e{; zJwm*p)PA5-jQQroc%1={$#j3eFBZ2r_F5bc*Z_1No?k*o);hBeobdSyLhk@CR&UCk z3IaG!%=lFo${{YzT#wSD+R-}2wo~lUI$FntOhdRnR!5p~dBjMrYdq^QN;DMUg=PWo zH+ZZoA-Ba~gIEVI*-OxiHQ690m0C3?XY#s$Nz1Sgfmf=Da)qaYAfZ=l?LNp|>TZTck&E7Hu9M`bIu$;K0U8hLHV`jYH($S)pHthj$#Hczjn(IGs1X@dNSIt zUqAJ0>M3c*Ue|tIqDAe&lJH0E+jOc#|AE$D*K?FKEA_li>m}{iOfG2;YSj{*!Z7OV z;1Tu*wpHI@MrhVb?UZFjCc7~NLH2v??)SV$yPLXK&bf%k$X`M_mD5!_{pbgSI1EGX_jl-g; zdzs7YVtX~!?36_L)lorqdX?Y2sd$~TF#4)2(qAdI0QGZZp}iikxjoWx?MG&hg^Ksz zFIqijnmI1PN?^3*1zm4!<{s|6En<+j#Yt=VA@KKT&lj~9Z@yW3@h3q2)L%LAa?_K) z-M#BKk7wyG+o2VBwf$$>b9diON6)|b^=FbNPNbiH9rbdZ)0ONpb`-V7fS;RShZsiv zK*|(mU>zw8Qt(toDsq&IbqCKy(6Rvx6O1RBGfoL8z;q22M>f6yr*RGK(P(>vkk+r$ z$Az@RH+_#!=$QU7n1dZY?Wbw8%i}WZLHS|XVv%_OWYZo!^&(2*%DQkJ^*N?56>fC+ zPTk;h?A+<_jai%)i_^=C3SY7~Eci!TK$xN1dlB+(8oNB;pU!Ox{O1NC0pi23=qd$p zOWaze|Cr86aS_kWwGB{M>N$}c<*;n^@;TYeoru5)!D%D`Z4Fma2*SYqcFT*HA`Xwhil6 z&&M;j_B~63Rr!!C%3$8*>!{6^4pRHkHRFp`Z(c{^>5`}&a)2N0fly zX_Ay%rfotvZPoGYQTWteHKT8AgPvd?_A1?n@dxwA8A^zdGZD!AaRI!b5ajMfyes1Z zIu9KOF@g7}ZjOTg3GXu~x6tq0*A5DAZjFawqN7vy!8Y6`4QDqmJR;>z2`&}T`v|BD z6g=3`gT6+jlRwm-b>bnt*Pd|6k)%sbNLC2{Hi@NrHKn5q&-YBX95jPEn|y5$xQBm&FnH+EWu*R z0ykZ+!meXJzlB*Hog-ZxHjnsdQnEy%c55^m5=4u|WU^Y0NX4;*Jq_0Ou?4-+ zXCsYPPrejxY_!VRvT(uMyR@GAc`(@2)?L_MYOPwmw$_N11!!l%EHSG;+E^&G$CC5{ zYtVzaX7RcN(b?D@G@B*2xz%cseU{W%C7=klfQMP)3%AV7e|xYU?Ru3!Fygpk}>m<#23Vn{-je+taS3V1cx02JZ2~rASv+LWTVPpr|v_T z@BCYyz`Op139{K{2jQ{8NsS67$?EWhl7Waf3JX%y8wn&sK8F>oIQSYElq`zJrI?a# z8>8l=(<#~EA6jngjTK|Pjb#RwT@)B}LS~1>1}{>ZJIVfrAMy0NWXa|7gYA&bR)Fg{9?1hkxo!8y$lx1gg_j!6L zPuOg8`25kR)0tqVE{?A`&wJD)0mOWHk7=JeRT(RL_*1CF4@0--C_Wudp5lO6uC zNVCskj%JGan8gxHv^J$2PHJ^W(#4jTRr`s><+3_|z`m`W=M05n9?wUd!5}Vw zb3Fb)*R{YvX@*gCS(CkEeQ|v#YY_x^DErIScn=J6f=P6?Sg)`)*V;##WQmHlNVaQ9 zp)1kgz|;|~(Wb7xl_0b(pVJ6GYsl9|sMCd)*;T9uw7Y}v3*Of(ro7PMT$)x zzjPdrV7uyEv2|rf$4a`0#$#R9RM6H`n90d{#*fS!?&;3P_Kb}$aR&l!wq$J1ge=hU z!?zw8*M>V+ZCll;2ilc%$=tDuHJGxs>>pq}uTHyPV3)HI*s{{lH;q9P4<#YUFSy7v zrVr=13A$e)V*IqZ8R`_RWz523RfbWzAfT(MucV_YX9!=`htbQLJ;C* z9Qmmy?4jNuZPgxzNNF9Ito8J7TDqhl`Ro?ADSLGO(19gz_o8cB6M;ta3a&w2VQvg0 zTD3j7DE&kDIWv(Y%s-qpmbJ z`s1xnV|$U*vvt1D*Iry=w&3j+bA3T|YS6)%EUe*U?DOFH+HeEZ$ZZ%iTCp?U7w6dO z^ZC}6U^6(|W4TJT$lZzL$6$)(&7EccM_ZFX}w+Nr@ zOQrhzw(a~4cbaZ%xodNMYePe!ZQfH*0Uu z?`wMl+TNIHxUz1}5-5^`olAx%yM|4QwT~?u*xo-pcfrPiD22gO5$y zrT&Uzxg8S~isdB%QtRi4lMUOpXJ@`Q>UKdYAdz~`mR$Sr^j^%pg|nU``H$zb#kb@>hWaeje9aUe4EyYYn8Lg8NFw z56?h9Oo_o64?)lt?kOk-ho~P{`qQDY!bMLs4}>R@6Pw-lJ^sYK?#+ls23sD#sBP}h zSn=Tf&Hdp$$@4e5?z#W|dt93k4GlEkhllldj1~?&wc|ebrp{VtWT1J+4pf8aX7~L& zUb?6-ck6km`lZQxU7H7M1CfE2$;p<1@K9}Vll$KFPhZ$J#^vnOcd$3ug^=#s;N4M$ z{}Z=m@TvS)e5LLY@mG5k&M5G`P_1`z>;$z%C*&0N57`^nZTQ-TbvNYl`P>b9^u}yn z^IW#mSd=^8*txB*pRc=1i_u&pl8Y|pHf#>U$l4ousT4J02Kw`ew+T7el7XV&)7&wTEWOP_D`h8ZHW6 z87mfU&*?naD=(yzY!#_KFun$lEd}w2dqcsiLSjGAvbMBWdpuPTeAI5IKM0M7>DF*v zyH5!#TGkBA=(5sfb~F8fGTj;WcuFH9Ww*ODI&zu6?su6z*<#LZhC<{ADC`poDeZA@ z+NNd0yq589RHM)znC*0#)rG3es4+y>het+BZg+WPq~!5D3|&-PYdGA>&-vkk*7NXD zZ-!=sdoblITuW@A8r?JoECapA>`!M34Q9|mGhiq2SpPvTKpyB%j9&)y)+obEaCH=< z$#b@w2`|62{UB_qAy2^OYzQxirIfiQQL(xFkz{U8q1o@H`E0G{Wft+Cu)%QP_SqcF z6!ee9(~jke#qRY-Vwr5`ie_1E?&@hPCZfKZna}pFS2#X+1*@MnUVZ+Yg*a5qYz;B<_A|HL$=`uM*GW9cKFy{OYvR4p5@x#Rs=e_4b?UrgCZd#FlNVS9X>D^OWccQrCM1zvE__m^Bq*Z$D< zi_aKFD_!e1UTF6-w|&d9C2hqsX4GA4!M;(eUH3hL?&WHHZ#&=n0Y#cIWpq-OLtIx? zfKAao3i$NVJtVoj52i<_d`4Eik?yLIL+@+0U-8IhcdGHC{k`KoLv5`=yVU`1q!|M- zQlC5C+uhXYwaSX6oXQnD`}^lFX>0R(+L~J0s-457m1}C%WP-&_CJ9r<-aIGcae!|H z@9MUm`Uj2-XbYyG#h%XE%(7^g;jgL)KC^3DwPDYjHIc_Hv(l$f#O8{Zd^TOzgVO^} ze+zP43jFGr?i0$bypz~LzYX&FetxpZjAZBMKCRrGYp`RSdF8HFx(se0OwP3M4#M)n zCTTvqbjv4~zw>91C$u=a(2&N~>A#M7&!o)?b8Rd49KN_BoVO>@=+DO-es|Sp(Is3u zYIa$Jt>KpZ*cE-Po7#&j=5Px*cMa?xVB%eMW3P$!i#p$+QpwTKY|8POl=4*1((dl1 zJuInLxNG#tCXB0a*djWuCaFI)Xh`_5FLh52u2e&%hvz zJuQzLmx^(@$+&25G+aUk82b5x(5X$~>wp(7bEh^Qg+>EX z05DXg%=@R3g^ChJpDp9_bItX6@0wV5^;muN%3$k))eBndsDAncJ9_FUOAHT7h2n~d zOSKa;x%swbOK+Rvu_w1~o^EYj!2j_vP4zkfO2uB>pJ9)81RT&fKi{Or)h|%kbJdmZ zPYnx(F>$%_id*BV6>J~m-ZA+MrROeJ+rNI+cRV1jS1m3bIAOdXod$@XPSM= z{DV`6IGe=Yjc~WkDhI8bwr+m751S<-_M}OW-7T9oTzvST-H%=ItJkkvxpmR}2~nJw zzi8{qb#U^;mKA}!jD^iIHo#~DSZVl2^ZAJ!r(XiEJqNJ_&H<>va601r{!H=y+Dg^}02enXN8|YS zxDfZ#or#F6QFK8A*(8{f&S2~|s)){r-747}mZdu*X;<{FK*(#0*qY}p*}lA~qw)M$ z(3zC1PB_sda_(sIHUU++E=W-`6Ca}2wl+2t%yz|N=`5D$9*Ai>&oly&UK}B! z>u}hgaV|wj=T3)kzR*T~rq4Nk6KC&~heGsiM%OU1mjY*$Gc(ADI@G}9>UrxXuFc}_+KF{vTDSU|Y%Z6*X7#!)x|=U|*!>-Mo8}(ar<27b{^^`R zDpg3OuJ>n}Gk!gw9sxVOt&w!pB)fcOC@1TpXt%i|UeP3nZK6xI9L+UeyL#R6b*ry! z%$*fwALI!9tsO%g(P|GGFVNotcJi@UDHbdBb;e?y!Wsp}jbKD{+I&C=0(O84aM}^6 z)6X5s;jbm@vei|fZ&#A*s2hx4D|!@agm%bG;19X(5!WK+`{?qWSahBI8PFU&6nGl< zY(hS)YQCj6j(cEajwhj#NkBdWH_9=l&O`FezeXC?DZC~4J?iIq_Iy)t7=ngm_RIL&2Y@5^z40~NT-O2P2d~XYh2Jo+GAh=aBuE*# zqtma;?8bL+PgaB1o%tR->wwaa`zpTLoL))#RFh=_|!#4~u&z{YG(<^_=1kAI2sxUN0siR&wBN#Hn;3WRpe88vcHp22tHLb(6psXfPB`*TqpYxo zmvu#WN%r3;=>GiZ!O_u!H}GJzx}jR#u$Ko{p50Ch)!Vs>Sp>HeQ;xc4n(9WU#7$PV z%GG3a6%>kS>ke;JFv<%!zeVUjt33MY?Mpj(TQIulWlvQ28O|w*yLQ5qga4iGmcG%7oQaKz(6%#F0l&iM*q5ILcXtcevxCb*3yjQZLIp`K+>GS8lef zEj5C!7|{dzLUsM#^;JFKr%pj%#dQOsZt)DjW}bviuMK(?zH53JC?_}^Imq4Ey^#L> zWiM9(R5EZYuH_k``5Kf7{&EGX*GfhYpj(i$Zjmb!0B!)otv~M$R(K%%xWCcm>+kQM zLU>K1Ygzo>N!O&J@L;*3@F($je_VT@(N)sUqYun+`H-I%(VnBD$Btcd%_mo8ExpvX zoJx^i$*txgdr#-pyGv~kmb6DplBF2aURZJru8bTRrq~i}(zKkGx5{R`{OD=ao(0{`b^&w?&1j^ zS$z3rlj8NU@9FnvXVA~O`5s)bH$x|b2?AYRCC^>Rb)_PYNEt7uDsa{Sb6qpT?~StV z>Bm>o&YaNgHFpMMpIiFCpG_{42K$fS#=qa@XH6xMKCbP{IzsezZD~9#`1>z;&ScVV zH#a!Ubh~!1+0mfO{9k4FvsD-i-4Ag z6DydMCzEc^ncO_GrdsZ3dGg)vK-m4J(;~5HcYW_w`|9<5SM9Ci+qr+it?aA96F8fJ zpN34bL7W`|CZ#4buC>ghxMl)p;eb~6>8uGJwhI_Yhk&`2;VL~SB#;iuMIuU+!sTnW0&m$sOh12TnK`vgRT$aAtN2!7k8BfUp`xrEu_ z{t_=v*MyVtxX1=Nv`=;N-9bF+t@N{7H3#6Q8rSGFlAtKKIdL8@2DsW@fi3F7XcUF*)i0{~2`% z5x3J9W;=R&v7lYjn{Ni1Ax4NhZ3|1&YNFzFC89Q$#e3`hu0ng#C1}s|_6jRUF17gu z?5}tZ?Q?3MK#dLuGh0%Ay3A&?`Rtf~t~MW9Ho$~`QKkIg#^CA2r&l zpFMY#&f`yEM!t>iS?9wIp9U9iQIfAx7oWK}U1Y}RMc0=>Xyj)(>27u0k&7QXqF=c@ z4u?kFAp1AY#@cliKig+`XzvA7n2nwHr7v-<+56uw%fX2B=p%9@D9d-g-{JK-jvv=U zZ`8xH{4L|^sm60F)HGf-y>TwKIDOk1amcne-?Yh&i1ph@;EC7r_g#yKG2b!ikJ1z1 zEW()c#CtYVn);hJCn9Z;NLv>VK-Awpo}9^`=lbhl#s}|7_`*h^3*e_jOmJ)g43Gnh z%~u(^_8dC?mT!F@-vrUeJ#{c8{oFz84D*~hrwJJ^*c^7~skl|K2fh(-DAw51aM=h8 zKa4asN;YM>Q2|!_$0kJ-zae4!?>7~jcHO|v{{EeZc`!gbS9El&*usO(|5zv7*cZS; zRf0X)uoglM5&Da8cKVY#TN3Ls3t zG4IE@a;va^p%q-qFt=CNxEmInW#E=Mqs)<64JO?@&<^%VbGPp^jm#SdcBFIJw@rQB z+!YgAuaayy;O_VQ>@~sW7T*z?K537bUN(7rW=1cG7OTam9^4sgj>ns0!iUaE!aY*I z0&D9re>RA7{9M{fsDy+)K*e6msjo-5(^r)TQGYDf5{vo&Dg|?k?{(s5&t0YOAzQ#t z62@_Ce`)*Se(W<_aDY8A_GfD_vHT3D^ekUiyklUHGa{m)<>W_*=s#U72e#!@(?tH# z8E4np505H>-KPxRvGj^Py9b7b26pc`#Uq)F$hTMK^3%&B*=$#1BfSmd+Lg^}PmaAb z)(B@RuNOO=ZSL+*bh~YmAbPz}@zSGz<$lytm3?kO?&vyGHIG~|{LY%$hockwZ(7A` zN5y*hQg?Sl-0U+;7L)9@7YcT_Y_dp5i2pzCH*T2%emkT2*ZW*YX~Dq%2l`F>U9A?2 zo4xRqg?8A{*5-iAkXg2P(Q)|05n-U;KY|UNpXZZ=&bNr!H;(lTcMT+s`$@?8 z8P7+`-5zyG30BjJT+aJz;L&Q7-@zX^Sd(eiuu+|{J?23fxGx+SxEx}bqg=$FN#P3k z3MQoR?O3y{9dv6aW6i-(vXZ1}dT*kdth%qOUh9G2n z?P_jOaCkh9_roPdRQ#N#vmGN$EcR1olHuOYXzze{^nGl3Q%rl;<2 zL>SSPUv%OAb%mhUZSnz^3ZK2OFCkNJ%np5>zy01OCxdAy=40;E$Qn9QaoJ^i5uR#Z;n? zYMPez_-&YiE=Qq^b00+=Ii-_BU6H8UYcfmp$J~_;YLC7|O4R0g{E*}n*cWhe0;&vn z=oiI!Loe+TiV#smyEYF;PP(l=cS8*JT_0Rk>9hDmm#rXtQ%g%`lh+-Mbj?X;u##nq z2)k;vfTb-8Hhh!*DxEjK4nAI2VO`JRc4_g<+~EQ*pHMmxm^qjLyaAt)#_uoeVw|tz z`nkMUKefQ1th&EX5t0_SQyYp~ZbRb}g?`!B2g9$gUg^y*u6C{4@VyP|x~hxwy_LGp z>&%E|uXm`@otp~}qvc;%wzSwj7u!sRz`{vx>1U4Ktru?v+uvJlH%n%(Iond0IsL-Q z6pzPrUNGqLhZZeZK7QkdeVf+vt##`+?b~qE+~o@vh5YWcX!Qjb&R;Tm<Vsz>J1tA~KwUFr>*{1$&eSO<5+twcuy&g&Q6_;*US}ZOtCSqocE%)_n*8lzFY;^LnXrs%OX~;&J z!?s{5NZW=|7GFaevo{d;C`D@&o6R!q4Nj96VME3z3mt+ZS<7aJxtGV;%Kg56_Fyi1 z-P*~&Kl?>%ugm7N1=e(vH`*E%wO?6cEpdN7;h{g${v_syTSNIoIG|WeKNbUtj4RyU zl2A%j7Eb44=42@wxBi-i^Buv)!S)0!)f^|jf<7&PZ*D=ac3`(|4LA`TTPDR-&5)%G zk6AD{+#7_?PwM5Tm_VSEQy?)Jx$F%xh+e@AQ@_fr;xx9t;S{p9tE}{xY?9%WfDN?t zZbgy2vi86|fA*OK3;Xeu_R)#-yknN{-Q05rEze(g>2bkH`2oR#B1PYIg?suB;8UO} zrXVQ~K|8G|+8y+xqG0m>@wL}FpDJoo4fMI<$3B6NMH~K%bCj(7Y<}#%;>;C_aYG~b zVZk?Z7bxxrFm0w41zxb)-3;FEbhkO|QM9iMe}t8geu!3Zi$?;EPQ$MEJI%;Qt21^# z!=L24jOsE*1@Ae-$%6Kt>x2pCSN&vVs7d%9gR{ARfnK_d2fByjRZ$b1>$$f*Cq~8($;fP!N0PCpi|1&k|EsnykB_7*^UwQMRabRa-}jMD zU+Hx2q|@nilF8(ndtff+zF;y*=a|VPnH&tmz#MYuDhP_Aa>ytkD54@NBC-sy$hs>E zEZ{1N;KyUv`v7${UHraJb!R4AyZrOZQTrkWCVmmi`#)Y zSLw1>U(=LJuaBoU=8Q5Z(dISvkyVY&wVhSfPMb3pjFRPTElgrWL!`!KQ6XVbD?(K@ zEjFvELQ#SatKS>3`^acU5LIW)>i5IS$Yhh1giG-S4BYOPE6R;*-@28(IaOH_6b&|S z-OB2!cuPRSP6aGtTi40!4!P}WvrTqZE@@r1ekmYKt-uP0azP2TgqI}h#@nr+I9mF) z+`9Q+*Z6_>V-8`>lO;b)I23P1XV>oYu3OjD;P!Z3j_Uf#?r7c0fWsW{wI^%)mNvIW zOa?LLs+{}SvA(6M>~I*vjgfd_Nu|wh0z5g7_N>~lrD1q)RDnOQlA3@WhQ9Cofv*+p zkhqfv4JgSH50dc^OG0Pxrd~+#vWmL8uDZI4H-7M`#$}E8JKfaZgg@;}@f7vb+We&O zZP*16CQJ|(A)6p~TBuK6z-N5x6>MPO8k0RjTZ#m}i5SRuSeNVK2p8_OKzu`OiwQBA zO@wP}J8NshU{&o9AKky`R8_SPs1^>7zK2W(sRMhu%~Wu=3Rb0u_V|Wx2^OOu@En~K zo8 zZu1$1yh8{2gjp^5NjI_do6n(oyvtf0wraPsAzQfGS}m}YAYfk zOE7WCc~+x1s-rXrlR=2_Dh*s^}6R!t!GV~MKEj|}_c(!(mR=30AVgEd6 zG!9zaHos_U(Ei~F1RP9iFo}MfTYIT-`@x-QzHNEa@(S2Ap5JH*xss{TiKgDA3ip;f zVNdT_y?H$rU?3>+ z2eaMldi!tg@2yI8XEz;NZkC0OvKhb-g-K~`ckf0C;Sq zCK?6z&P$e)C~oxg3;lEkzWlw=NT?K(C@E(d+)l|FGRDs82nifg@V;ML!e^ebdbCsg z#=@<=~wD)MI*a>#L zke6li@7(2{- z_i2A3*M4UCCpi?5<@dc$hN=`(Uys4Uz(4u10+8eplo)0W*(za{q0KZ?k-(n*8W*J*eWl!=;zD|q&vBdhJekcv__fB$at*olInt$B#Qbpu!B18pJ zXsaFj!y{OyyO1^lHP!{bB?{RCwVUEl_CsELK6JgLYdhPpb4?UVKC74(aR~5>d$ex) z2uC2@_XkVfIg`k9ImloVTPAd$tBRZ2Q!8rPYv`}X5srl&$xQOjwn#%XintbAnk?Rc z*KEeY!Y+ur%iZ|9({FXzY%XigK|lPpikg}wH8meD*Xz)J>ImEIVFv__+EYGM5{+JC z_F78STm)%9>R?{@$h)o9Kbe3&WrEWy_DW3pGZ@DJkN{DPsa-Pgq%~9LdwG!d=ArFd z(k(Wxt+RdKexVmzZYI5D`w-ok9=xa>hQRvTfx{^O?}hyj2)HEJA_>O4|7U!;;EQWG+z6b8U8_ zi-F>zH%7-KfLs<_z#GmN>{H)7_(tC|b*QJmzvs{t6Bkn4)(m!`g?`E(NN@JXyKO&E{M|Cjj$`jDRPO{%4YD_I4c0IV5+ zNY5p6tw0z^yt6;$6;nm+(G)W*q^|g}_IXfQP|_OM4(PqacbaYUF=&Yv71aJtRj^+^ zfk-!kb}`JG4NpK;FMt?@3>9PK*MbEjPXw3SgJ*jW;zhm}&zDZNvurw9GE;@16z`3c zg5+|Br35dpiBYT+`h_L~yH68y&_<4IJ(aWkjLxIq1k3tT9^t|H&+}%R>a_ceU}4^)8YNgq!X!cj;=0v%iU~%q!(p;% zu$}ajBL`>Jpxt2P(&u#Ch`cBlUX#tLuL>$uvhjDhqrPhPTbs)k@W&zA>#-uFLAfuW zh@#R2Pe`Y;2M(uke-QimzubB=-`>5%%_XxlZY^)wh3Hc7aCc~fPNUr+$-7!AtTBg0 z1eXlQIQDSJ;;rz5iDz2%iTKG^&=%2{@VU$F5v|Wq87j8`snp;MHmTNnlO)+45vz~g z>a|uRxMZy}tLdQAAZWd}Zg3u@*P_3h#~_Cl@t(j-C)18Z5lztTA;yrv-{2ubU8%n$ zcr&55$#nw}5vT`s5g8jAVYiHo6z&3_DE322kj-r0`7F?3vtS7OiG3ua!&eDplmm3` z=lXxW;;i!mX|vkj;35d9HVS_-?rQMWxLsA=`oQt=vF@Ip?lI7I`u_Nc&;==p7jpxq z*s!s{%7uM~s0*ye)IhW+nWiQcjb-&=vv5nItHfHd@5wH!+|bwmcLnXQrlI zgkG_^zP7f0Ga*#$YHBR}-r16@%vAC#=|aQY$+-r~lU>n&egBH`xt&yc^Z4jbM-OhH z3U|&Oes#SQ`6~h9Rb03JAq?=p!RMtBn*Y6Mag{8AR-g?XixE`2;MR3GS~-m^p3Dn% zOMDDcVBBOpSV+Npk)L~s(t^MMrsWxWUSj*-be*4PQnbQH2oT|Vm^bj>etMU|B8X;B z^J55lBbZFu_h)9G3>k~yU|UZ@zUxTQBu@5j^cF18`YKHECaDW zI>;Au0Y6D!2~xu8>m|QERol_G`f1?exh$uDv3hAsRm2_=ufu8KOTETSa#?N8pnY}M zE_R-wt)@3t!_I73T3uBa2y17ctk%wy`)d-Zm79@DU; zJFGj`ezEb3?DVkqDvZ+1$zT35`L(ZUuRizO3okJ9FMp}Mn$*9J4$zA{49=bWb1`bp zTA7itCK;`vi){P1tHc z*a0d!k@*;(p!2Xk#GoZ7@{hQP1Z26C*5Q1gWP=ArH|3UfY}($we|5t>+B5fGJQiro zHZ+7zjNCO4tj?s?UMg&KwD0Zg9t--W*Dal10UI~|^Y03V?3Pw+EuZ|2B)2zJw+D3H z+!^4j6!BgOvwbbFZ;w&5UqDb5jbzCmf~=)-Isz=_zwRXpktB&aisK8xZ_cLc2)U6Q zHqT~EpyV5?N)SZIqL3csfJd@7fJKoC0Q7|(&I5u$i0w)zy6C}hquB!e$}u?zvu)9E zOm)ng%ou%6%v0@Ai(T|uj?q}=ODuNDYew2>z3PdxS;UiXE-q@vt5%l7Coj2!f&%RD zI~?i$e*X?r&}G_Qz7p=bF5`}Xelf7a7{p0(aBxj9nA~ncymP-rk&dYb7;+fIV*oA_ zoazbs`V&Cv(jMjBpdA?2t~1;ba&uqE`sm{OeQz#(M>vGKQL!tCcJ%kB9S*bu0StD8 z!mJuW19k-Z``{dib|A<0WH7jP{~(%O9PaR45?+VjYz5Z#e$e5Ni0LyPa9#R*$BI|n zu8R;71Y55ELMy1>Pb5nh06&#T$to<063JNc9=vV}Fe|gKLL))jFt0A4&z-#b@>w} zi|Vl0jVd&0bgEi`KXICrtSDa# z&{$VMX4OY@ZE09hL`r=~(fq-b*Ecy50UKP!aYv?67+fh#Dt(mcXhO`;ytk?)k!X4E z&a26vv$rpBzsv2kuG+OSo=jEUA8Tp4d1WS%$lTv@<(2t!`Ihy0zkx9lSaoTtDs$zP zEm!Y+Z=kmi9-JuN)YRne-MKQp_rRX`{YkJAt9nrwH6#-GH)Opx(AOJSg=ZoiM$tyY zhkcJw+h!;$!d3(1&Oo4y740{ItRH+%5zP7Pclhw^ww`5$2jJ*w^T5H=>;djOUb<0rs5Xns>NJ_d zK&vt&5k zJX!oGlrEP}OJ@t?;w{g$xc#X`&+=~4P$a$mb6E;{)A_lE7oYc?t`{=Jta`ED;&YT5 z{@txAc7H=z`)3bfti?RK%>)QDgrsHJ;wgzuF@NcbaNqm}_OTuA@nh4& z-rcN!oBO^eMsBH(huu5V8^p41gHv06;?V4s>>g;qx@E7n`o2$QGpXTy>;Wg%4w6m0 z{xJM@mtp*u!4?uwZ$SA5K`gxIl=jF;t@{+~J1O+{&d>Mu3SzHL15g~`!;nA3V0&5v zy=fYLuahXP3+5JxZ9s+->rc-9BnrSDZ%E{T9f#`mf5PNU5-=pG2?1~TmK4$Lx(JlS zd&T)gLXKi(7>XX&_6;(&p+ou25FQG~+p(z_FJZ4}k;VVK+wQb{&f>hrX}QPjx~y2j zB0NTgoj{|YPaODv= zKTROLXS3)D8e1gSDYm5yQ7;lYM@KR?3bvy|?YQ23>)4shjoEC$l?R zS*cW(J@f9{P2=-5*rNCAv^kU=DR7-R(v2-h0io!YNXnpFkdiSX&{fXak_x_K!aICp3Oes1n!!GweXDH;FfaBYz7uMP*vDRAQw~w_e z3nW`z-9&5aqRF7MQh>ln!7h#tK<9Q z5>2(WrJ^m$E{a$Sxy#PI2hU%*Y&<&b>uz0_sIJ_yw{h>5%IeB>*=}EVowRC|REI?C zdR$t#wZ666Hyj;bw(@*7))+~(t#3<3*zHu+3sh%$Xs~Zdb>+sE#TB${L9@{x@O>0yY=S=TWcGCA!u|DnYo7z^`|sjCXRxlj*D4Oj;H zH?q?gVR$0^BIYhl=4t*#d@!uq2IwoS!0YltmtO&?09gk?Ka@&RkcVt0%o94oa{@yF zW*Lbj{^2*^k?@JV)5Fs2vsY_f{1q+pAbuLZ{`If3r*J47(-UmRT_@6o&+uK^<)=>7 zojS#KoWjrNi~rxphrVz8LwKCOihcDUz8O3{G1YCkmL-&y4RTRqic#kEkrq||#R+;1 zSE@)h3o5@P;94+)!4NI}0!Ns#Q5_{IE*Zf<=z|NwWZcd3<8UAxC+oryfRL<=*6)w4p0l>+o2-mENYL zBkcC?@+S}|(HIQ0zt4?;f<}rbBWw&a0BJH|U3=K;vS!iY zrvUs>HNlWlwwPtflNS-ntzs!=U$uF=|gLUi>TXq`-Lkt(&6uq!uSzV)l~Fbgv12wvufB9{vLMi6R{~2U?0GY7Q8&71w_NG(jpzd0fQ{-4Obq3hUVog+C9YI3>>UpF2^)idGSm&=>Dr;+< zfqnb5t+g!)wzaZFqeAyUv9qlu!BJsLg6iM4t?;>8+~L~*Spn4>=JioQ!T#)k?u{ZU ztN@RE5p?c^&qDShIpU8~X_+L|DIeB1?BNkel}HbQdmu830s#p82wa4>Lh?j-(vT4l zsEaQR`!CYJK?H@uB@v+ytAr;>mEjWA1tZO3G04DJ(B0{d?T>jpu^0)mKXOC#2G)O7 z?5ci8ZDXzDBe9RTJ8L{Q#BT7~?cQczSKpE*zu#XQtBc1SwN15-+F05V0tQCNo{n9R zwuVyH^aX{>q2u2Z+oQeqh2(wqy2d(tJpSkM-@B_j-TZ{x9UF|n{Iwr8jV}3_>tY|& zX0M4|Gf4W)n_@S4SoIP&KD}K_z3h6Q-`}*Pe~GUS-?uMecRKBLu@*?XS-v&wV(W<{8h>mABcas>I?3UYDPXuGHAAsBM#Z!`WW?%7VdiHuDj?Y5eSSs@ZtB5 zg9^Y0`!+b}+7Z8#e}hUCR)Q{I2epr~gKPukMwUg#%kRD$?IqgypTT!^>2!LV&i}t_ zY8Xy;SbwpglH^kY0SJvXRtViq8Oo0Wke4W?pB~V)m^eCd;ht=Jdv?!-FJ82}EthNC zeUWy9=MDJ7$sXk+sEnpUNiT@CgZQ>iT?4u%(H2UNJ8*VeB8oogn1g4ii9KAl7Su&6N0q$ZurJRPC&vDcqtbZg_t3_ zSQx@-bgZoe&_^Vt5W}XVtHVk56w#rbb;MfXSAJ(NqLdY8e2JwX14FK*aX-H{Xm#{D zg7Y8k?d8wgrNT+t2@D(`2YaFEVnX3RV@*wcO-)gU5BA4Cc6EJaxgWr!c8AYx@rZt_ zKiJ$A3rJ>#Hv*Q-U-*06Uk~I{)y;zTP=yc;K!XU>%In#4t_s-~PX$=esnt7!l0R-# zy(zcEUeL-UheP7El2vd>gRV#+yDJ?1stvaD+-5K~ z2P+WY5MI}aqN@Ed>8?^li;aNc{nk{{Qx5JywOE#ce_ zb#B*UzHBO&m8Fa;;q&-pPcF;3jxAdRl>Od0Y}t_$hsL^ld%MRD6&4a&{h!X9`4h8M z9;oa^LO5Pi^}*d2J$G4qV-3L#FnJ_Kvp;gxKLGE7O?m(+`cfJ4qG=`_A+ZYUD4g$ZLD2VYx-mT+Ir7X zxBCcV?z*-0)>-VIdPb8k@Jhgj$<8Rrck2y?Md$PZ#vU_zzyG;<5u3UZD zYW%HQ$~NS3cI|%NcKLyxo&%R7L@y*Q*q6R3G{M6u2TqUl^W?+30hm(Tu|I(>h8_Od z|2HywZxDsh_Py|rsYn!PuJr#-Y2nVh;%DMNh}{ifQKPU9NAPd+_j6bk-yQn_Zrvpa zh00T>=1-ma>8Vp%{H-bemk#~MVk~rwJ!!8E33j-i8?BO~4GZA^l0wFRO+In$s|L~2)|xaqJladw8M*pFZOnhY zT5e*mIfY1bCH|djLY3^vy5DVLyEhp)XWwWG0L@rb4ZsscpR_cM!E-G5i51NlpfRMK06Yh=$?!xc zAbO^|r<2o;@QV%rBOcy;XIICPT@wRaHUX7KT~>Gw_DYSh*h|F^cElHp(dprV3v(Tr zh7D^sFI6gxU0c>{Xw0De{?n<^HlGzyl6<)ltg_+O=GLokxH^+=4SU&ZI|=2ttIf=t zs*>Hk=QTIf)nBlGxPt7y-|)wL1O>cv>*~IS8p{S%-C(I{=v%#2FRm%^CZphNX>kh1 zrg${j(w^Oys;;lEPVLLKw^WtZbm1D<>=Ct2(o!q{EJg;qiy}9`n-dxYjJ#MkBGq6? zjA3O%^oA^jPg&Sj`H!^KO`DqGIl&&FL-Qsrz#d2}N%BH3PcGpXRW7a6R^z}QD|GY6 zwA(z9NNpq{oJ8irzG6v!8lMn)B@!L6`5#r1dqpL}@FfeONNq($ZA5?nPourf`W#h3 zHCO7AH;`cqzwpq-I3P$CiKM$Ea>5(NJ`UT=7|br^7~W_$#yS!2c2@saw7$D3vk7wVauYE{@Z3hXds_SK5r6}uz6H(FmGo&SyCUg=B*5{W=g z2{tta3p@Guz3%0;waeXJT1qfhe+c@%rW7X@@@ojk&Qd2a;(%d&kA7Udgr=}y3P-BT z3pa&R2xRzz)%bw?Gm}LFC=(=D?XJ*mxAE-_h1bH>)nWc)fTh7g^B8q$>uFb*_+Phjy}GXgBEe z<}o9LrG(*U@qZ6dFT@}3MSR00H=RQ z{qPQSa4ni@lz}j^$TAzfPyu`kvI{3IUzW^4+w=)ZN_2?bQ6aWtoWNuN4ywf}b@(mR z0u~9)K|=p>lIZI*7FQ`>slhnI9|nV74Kc|`5%8CnKIosM_8#y|xJ2KI4=Roiy&;I? zyU73l4Gu-ZmmSt^2&adTdd)I7sWpJ2Fv2a;5XXjPRgPBx2cXANZp@gN zsRj`NWC8IY)>$1XUz!_DkQ8!=50yqmL{vvLZ0)nyyov!e3toG73eg<6!4OhpQR%gm zt7)^f1{k4c$+E>}Pa1>^Ksu@-Gqf1wX1Go&NtZ9^wJWgaVdg%A46i9}Q_90Y-~|S4 zlcLmkREsgZ&TLr&qEU4=h*A?QetP|Z5Vl)0lf{*SV&irtBKxKD6=SnOYF4;pl%NmY zV%FYs4nC_*k08D4yoQGJ*o}4{zD1}oTb3KcCUXkn%?xBIR>VlSz~XSJTTJjRHVcAM zXB5y25o@Q(41hhw0C!HGy)S`h<1&5}0C1T45GFPbV-zE~G>RtAQW=J8&R$N>1V}ik z(I}|Ra5V9D2(a1?s2q0McIfY!mD^1rQ$Sv2LRW|exF-Rs#-ch`su0vHwfbPRVmC)2 z=WSBVo#8+jfxo%AU*#6b9|{Dz4T{tR&)Y_as!A^VdO?=NZAK4T$b~G3WP`Fy4Vt~G zqT;2RKhw7R5P^st+TEtz&~P5YVb`CJzQDB7=g^mhZwWn!z}5o)@HLo2q(jnmNwfj! ztP5F{fN=p^Q{0PalAJYN12a-I>O5y`r%Qfdzw)2} zp83yH_Os>m1U;9&2@CHPVtOp%2AxYQ%Q+y>$Fc>!i!{#oEQ#g8s8zYJFp~xcsMK(; z;52)>5p5nyfAZ#pk76$MkUz$o1BC~C@wksKEj%7?u8;oor=E%yg!y@zJ=#0okQlmZ zDr+%zZENnhusX7Hc7Ky=R|5D!aGrD`gLrJy!P9vfq~dl_?~eM` zyMf)!zR14L&LFbYGwen7GxjI;52!oL+yif?8s5T}@eOAkdq>%2R1sNjT94IKL9t^%5$XGa$C6Enitg)#ah%cWUClq<)KyHM6^XVyk zjSa5VB-m<$N1_aYFxKpZAtDf7LJB)T6dva(IzABsYz(Ns0j*;1ei4A?ShYVx0Ie5sn;fr*|43fAY z(=8je9tbqC7vVOl^5o<;^ajf3JZ;cvRG}W~&xC;e5Z!|o5haRTIqF7wQUmmjDHJgH zvTf*P^ex(;gMXt0`UFk!W{Mxw%~dISNj?IQFrfeFQq};2YN&(S5Hp6}To%}2dbeeA zEvTUi-Oi|A&?xjWeFyp#=cpoq_CfN3j>p%~O#pHEa1(jZ3Vefp5I~F>(3n)ZDj~Vi zrC9CaGPYY8G<%ay7oh)8BfhnzHSW&Pt0Br90}f9MxJ&ekyg+1TvJ1kSjOXmN96~f= z2!hQ5NLE`8k@Jdzo_CX3YEFZS!FOg`>lmsa3&o-=s5Rw9D-xu3Km#*zymg2);xc0C zpx;3dczxsy8K$8QB`+CA<3ERIB^u(abpSmWR=%XZHbNnTQAfY1m7LC~V^PpcGYm2; z@QH?mFP1~Fa;QkdthtK7&0?|#P!=%A!#fg4GOHwWFc!<|U8dWi)kyT_a2O-`X1*!~WLPwEbfT&;6TF3XlW zSXr_v{3D(`qS`Z6OmJAYLBmlaz``8aaTge!wa{6Vk5lrOesKJU#JOXnIypltJm9CUnm5{_v zZH`o+EihLvmr{Zl6A_~uG10+?ip<7Um%EwQBl>_OGGn_Hn4&hTOT8X!zmu8byq1a8 za)n8-A&@45)Z0?5KFX~ohaCdxN`}DG^-#CrKO=Z7enpOYC{{FIA_$c+YF3;{l|mIL zT4sz_2ngZ|TX|@B;>>Q~egW}^d0b$A6N-R_4kG{@UWtjDq~C4~+XUw-r@6V=D!Tg{ zj50WRj&B4N$LyR+u@*zSWkuiKAefDksM@~_GdM*O;Q%5U0lLWQ z5b&%XOo78}mmKLY87iz+o_2g2z|0Mih|i-lrd|*x6`(BuZ6q6nl!&mR3jW1!`Xc1n zoDsLK1>k zIFv9kaI5UMNI$|bl{rSIh>0Mp`vj{5< z<{(g1X+Xm0#Gi6)+8)moXf3VkIFL3&6plSLv|Wd2yqvfT`;+skRl2+EiohrbG!Y4ct4^9Cb=~oq(-yd<< zSST3t8KL>clE?&qg=$Jf8xcP-4Et+|cPR{kbZY?IBPy6m=(Kk15UR7sfPOMF0NRto zzKBqUD6RdFg~3|aa_uNgeW0vhvPY7w+D|yEv>PNz`wrx|IxqH+@FZ+H$j&VbId<8* zw{;Up@K4I6)+z%c9%AdJ6iFLE=#Wl;Hs2KImx5~o*EM|LwXqRGQXd&R2CSX79O=Pc zTmapfu=J2AN$K#v7%yP{<8$ug9;Ya#E0!( zr#}XlL$3Xc)fiseW3o5|i#4W(%1>6>J*rw|ir~46^6W?WSD~*VUtFTk^xqPR1Nto| z!~|c;6?4hO&`-L7H92!Zge`+NlZ8iqn8(uZ^hHzIY701cap;GmyIeft+I5*ni@Rql zZ*!ftKDgTKOfP3}l(DS(hV8WLwn`ql$z-$!9A3uiEDoSM-E>=@c0HTwJE2Xn>$l{l zB2(Tg{BF2|W!?TOyg1DfKL&x+d-*Qi#w>~!!^JEOk_dAP)MINdgLE2c$Zr6x-r{4z zd8V)l=n}V>D&WkTR#lv8HzQeLVnZCV(1r7`s=~|8#PX&5MfsR;1wS9WA8-5~o8K>s-1rzRKmvyXw-r}0&*WAx~v ztd#VrPv3!6b8bHWH5R97mefOAqYY(VES+?%4I*?j4zIHnU^*0qyQRr@wxB z?_M1C2t9<`4*>sS^b#G(fBqZDzxzLszpV}Vk!a1btM;7Uv*+}KbkN)UZQ%jnRGa|T z+O=gj(fBAzy1^BIE^pN3b|5(QG+Ek_M2vMWAB zx@=FULW%SRsjH{yJLsJ9krxhp0?tKCgr9U#Sq5P)KzcvA(i(ssRa9!RxJOJ8wL^`n zcBdcPjn7(d_2Cl$Wc~_Mhl;xwDCeZG>w&~ z+tcOxfj#{H?JM8f-Zwt%f0vqrHhrPI)SU8kaU6b6_^z;7*UxtA=)&Y7OAuV-dH;V# znP5eO6R=OjdqYW%Hw3kZkN67qz}n?~`SNhMJm0tcqs#j9<@m|>FFS9`npHJ*bv3Kj ze0I&sTKueB!zL5{Ta%wu0uJR9RiE&u3ztRX@n{v)-La!6gW`*7qGUP0gUX$)Ku^qn z*^klydQv`_yw$%V%`S;vuxnGaD!OAw41*PNlP4ix+ogL_mc|_MYx)>14pgw~#8(k( zRphWdG4R^7xM9rce>&rb0q)jU#8j40lfYn8XJMRDJPjW|ZG=rIH;Af&up%A=1z-|_ z>xuykyi{mOyU`W5LL0~DXBks`)|kuG-U*--ui)aKnvGm`a^USD9vC7zVK=Rfw8Xob zoM9u-^te{l5y&DMufI8H0eHVD;BwHnz+xa{Qq94Dh(PK#e`ZA>up;BPAw;?u2%1$> z#vmaQl)ZY+|ejG>v%nn#YDvCx8wUnkkO#C3Q`DGI z_4`zh*#xvfthtCR4XwEa+`{}nA#yHC7*PTp37<4j=uT@i7V`K4KEKcHL(ECgID*OQ z5>i$(aK(Y=VY9k@5noqemmhAb;4)?SuIr7GMF9jkzEri?+i3fZ+ND+u=Ma{DWK4d(Kw-3Sia4#SNN zf?$?Jt3wUhT{X_?fWrm^U)f+%twPq}@daZmWlJRCG)XM%aJmB9jW#QQ&qV|J$LI6; zLp5GF)R>qI$+SBdtBCr2E~g0$t97f{VXp_lDWa-?*QrX(9}aq4E{EH0wg9wBRE*}* z{_~LVxUd3J%oI@KI%wwv?h2$Kkg}2jlZJCqkxBYH+Uy{RmKG9#PA^_z^Ly{OY&By& zQ#Wq+WxCfj9~?2Yr985uwU4uvNAFOul^#cwos@xO2*|1Y8c4Y&1tug0~j<2T3Sv|mGp{rdC3 zyV$Dl*WHlLUw|Ha*E_ZgID1;A`J2+kc|;CEEFD@ze2ct{&r#M{NrFuD>Lwu zhbd*14&s48Ki|QH%TCSJ;XlRIzhMrZp$a-hd$*TYlp9 zuiT#X*|nE#E`QYbXS=VgWdAwMU(&rZBJg@S_a+F@V6j?QKvO9`sV>kX;h-m@ThL%? z$YfEo=u_ecv>Ed9;VUgp7XJF92C?uBz;B4`(iaMaTyB`O55sTzWUug-oR)$P56pga zXW<06PKh6v43DOAHNl}=O(>tM4j$0=-7~_ULF*DnQ3cv84fK6-nD!yQ3joK$NMc4H z#4Z%>2q}@&#hd8uoXd$_H&rG41mXsx)~79VS)h6|A$+AKn6>l6gy9C&BYXBF6SP+x zuK)2WvO6NSZzahYESBI&`^w8E8G>HvYorOc+r~KbX zchcenyVQHh`iP-3VdUcz&NLKW*8bi=@WlLNgI2}@_1c3v;1cccFAHTt1#C9R5)VWS z_!n4iWU^H>3os8r!H~5I39L)Mx-SiGl?Wk#p<#Z|ckaTX#H*3EYYpWSeIPH*accPN$nc zJ7O|CwEY8B)twy!L;p22(9sFWc4PD0K4-Z-Su;>mWoNey@7UVX+KS@CJJy|d(4DD~ zvzg6X_mAz~IcM$L5WINPmc57fUzF?Y%w4qqA5o9DHe&WWoK6S8P0Q%5;bH3eU-Wg= z2J5-1;%?l4fK5Wqr!q2{gGC$+P=Sts9V7=>>!LLht*S)@05n1#-I|sBn}mApIqf-C z|JT3%?QhrE8xQWPcDpx$A_yens=7?It?t9u2b!w<7NaBmiF@zmdkR0g?mG78!ncke zx7i&2#tfow`^r65+jg6+-s`iRy4Km^XkP)#D^;J%f95|24i9!jidU0jth|F=rycHK z8~L+Kv>9z?3A^A;`7kbXjQ^}dJIt=@(7v=NAMwGT6MlhxwGx(s7m>~X|Bk$f_mw1n zv~CWblGs2!3>xu;#BCB|NdXv$xUT?qV;h-Mw!sgCp!z7b8QgEkERF^@|DE6S=ox=o zy+=xZ+GTcBhkm@zj1UCoeLoIWJIt<6Cnb+JpuHv=zH9PAu3pVe_sVebF#X0R4@*w3 zQyP|SzcJZy?Oqe^Dh{vdy9VuhAWA{VNhTa?3^K^cn>x9wihQXcZ&61_%|b;w_{#0B zmX??pd(s~c`=3OjrNwpoE5WqQ@g;FmVrG}=_i&Gp$%Q(>Eib^}!uW!obh+?lw%?mv z5Xer7h{dErlnIg_Ts0)y6<0y#t{Se9i^~G~_6fwcT&mM7eRyAQw_d%iguw;+4dk8& z*fJPv=ua|q!xi>AV3Ua|LD=}wVhqO7OAZh0&2@C-_71!PlQobh7Ydr3&UNu8 zFhgLQM6L2+=K$S=F{n#ofbEM03O4PJ+8-5>wd= z6D0Qw3yIAL)?oP5rwoYJfaAHeP|CK8^6x6i*qzsv(uA&a33>-wMto0I*#}B#hIe>hDb3*l z_e3cz;QD`*(jwBoETtuQ$o->~Hk7%Uqm(w4ZDYw&S}m($$Cgd!2WIm_(fvoGV}p|u z!;=%U;{!+M#v17Cg8azb_`vkJ3+GbX^3yY8lM~T&WAnL-tMU{1>4oZME*@!_ogI!2 zPfs3-u0*~0@$t#%)b!+m{NQZk=A6;)8oIHdMnTEU606;SG2-O?~>wXl+ zF}MUxB5dfeew+oo=>V?J;d;Z_dl#U@2(Hn+>9@`CcBQtV)HFaj#`Kz_FoSQzM0vZM ztB`X-&;91s%>e9u1ZGaN_#H-Thf(7sLSjS_c$!*G&oYkRNnDxI?;XIEL0oSv8`Voq z!H}p4zYEW|kiSuX?n9+{jM1R`OyTp*KaY_r72*)6Q$RK;&_&SU4Zb!;kr# zZDCv4HdyTJ0FLf1wwvu?d)bBTBDRkWu>EY14Z)UYn2oSeHpULXC1{)-ViRnVO|ipl zn$56T*!f({jsTDMD7%y$W0%3|=Qxl&PqI_M1ik_mKv%J=*?ZxM{7>xt>;vo?b}joL zyN+GYKEyr@>!6RYkFp!t$Joc&C)iEwW_Am^m3@-khNvE&0>=97><)G(`wY7aRz#m= zpJVs1&$D~k7hqBJC3YYCGP|FBg?*KMjXeMhqX*eH*hB1_>|yrL>=D=-J<1+qkC%!0 zp^>~eo*x)C3{Q>^<)_u+N5kY)enJ`EQA8TVS5KQpahTPwO5g(WtbAl_VrcTnj4(DiV;G+t znVg#ykBkkE3DYw(QvT4u*tlgnKQuR(Zx|e#Mr)MW>4Axv;XIx#KYj7oU|ty*8XBW# zQ)lM(8Y7X)FD#RIEck$YIJfUFJGLW9-JJ?t7z%iM8lE%*#1eSm_B^iyl`=F zbYNDQL;EHM4&{}pfti^jlhZ>+bP~R5v>_u5%w1}p8AaO;HcV50sxwnqFozn(^TV^o znTg3G!{YCARMGwYL#JX zdH_X*=|lT5w5IchK|Iy;fI3KHVrC3g$I>qkjSU={oES1p=V#}pCr}mDSR8gVm@M=?4d`n;(ZIM!-(;Mb9hffWKwn)f zPUJ6{HOx(oPYw*J#gB%e>46cEy4`^Bn{U`}#OgnVmo_nL9L6Bi3(HhsY+}S%`fNx` z7(X~p4WOimZaFGljG;3rW8e;|VH!+S^EKiLM;wdqUabiB#K@I!O1DH({4TESQCU)^e9}lKtnudd1tZ}aN zIVR2y?4L0d=YlkiXPJ~{Mh8%P9<#uJhUGC_@)H=Hc{9c#y&`?YOZtg~(s;ReuX#(U z*NRoC`Y@nP8{=SZc5GZmPhc+2sMOmFa}O_K65TYe(zBK>N=Hy=S{%YrLK%I9qCV1B z4lGIom=$w*>)9!U*D#S6hbN~GiS(LGv(sY(6C-$E)8~>y81QKLArrluVgs$Hd+bmH zmWl~_b@t+E@foNd0iGVxzO_;!BKg5dMrON zG$W1R*``d>`Kj@v`hDY}{KTA|vMqXP)OvI2jp~mg;Q8=sXJ#=cu~^RJZRZ-TXW)|u w#nJrult{~hFgrOV>F*ee`tYnsquRFcPzyaeI6gU(H|fV>OC}GNZk3h&KPG>EHUIzs literal 0 HcmV?d00001 diff --git a/src/main/resources/scripts/layui/font/iconfont.woff b/src/main/resources/scripts/layui/font/iconfont.woff new file mode 100644 index 0000000000000000000000000000000000000000..acd7ed6082729ae968e44aa9f676cd50a37e8eb6 GIT binary patch literal 34624 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 H6c%@&y~x6W literal 0 HcmV?d00001 diff --git a/src/main/resources/scripts/layui/font/iconfont.woff2 b/src/main/resources/scripts/layui/font/iconfont.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..5badd6e38ec077cd5b50da3d41f029f7534be086 GIT binary patch literal 29736 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.timeout/4?g(s+" is not a valid module","error"):void(m.status[s]?y():setTimeout(n,4))}())}function y(){e.push(layui[s]),11e3*m.timeout/4?g(s+" is not a valid module","error"):void("string"==typeof m.modules[s]&&m.status[s]?y():setTimeout(f,4))}():((r=h.createElement("script"))["async"]=!0,r.charset="utf-8",r.src=i+((u=!0===m.version?m.v||(new Date).getTime():m.version||"")?"?v="+u:""),l.appendChild(r),!r.attachEvent||r.attachEvent.toString&&r.attachEvent.toString().indexOf("[native code")<0||b?r.addEventListener("load",function(t){p(t,i)},!1):r.attachEvent("onreadystatechange",function(t){p(t,i)}),m.modules[s]=i)),a},n.prototype.disuse=function(t){var o=this;return t=o.isArray(t)?t:[t],o.each(t,function(t,e){m.status[e],delete o[e],delete N[e],delete o.modules[e],delete m.status[e],delete m.modules[e]}),o},n.prototype.getStyle=function(t,e){t=t.currentStyle||d.getComputedStyle(t,null);return t[t.getPropertyValue?"getPropertyValue":"getAttribute"](e)},n.prototype.link=function(o,n,t){var r=this,e=h.getElementsByTagName("head")[0],i=h.createElement("link"),a="layuicss-"+((t="string"==typeof n?n:t)||o).replace(/\.|\//g,""),u="creating",l=0;return i.href=o+(m.debug?"?v="+(new Date).getTime():""),i.rel="stylesheet",i.id=a,i.media="all",h.getElementById(a)||e.appendChild(i),"function"==typeof n&&function s(t){var e=h.getElementById(a);return++l>1e3*m.timeout/100?g(o+" timeout"):void(1989===parseInt(r.getStyle(e,"width"))?(t===u&&e.removeAttribute("lay-status"),e.getAttribute("lay-status")===u?setTimeout(s,100):n()):(e.setAttribute("lay-status",u),setTimeout(function(){s(u)},100)))}(),r},n.prototype.addcss=function(t,e,o){return layui.link(m.dir+"css/"+t,e,o)},m.callback={},n.prototype.factory=function(t){if(layui[t])return"function"==typeof m.callback[t]?m.callback[t]:null},n.prototype.img=function(t,e,o){var n=new Image;if(n.src=t,n.complete)return e(n);n.onload=function(){n.onload=null,"function"==typeof e&&e(n)},n.onerror=function(t){n.onerror=null,"function"==typeof o&&o(t)}},n.prototype.config=function(t){for(var e in t=t||{})m[e]=t[e];return this},n.prototype.modules=function(){var t,e={};for(t in N)e[t]=N[t];return e}(),n.prototype.extend=function(t){for(var e in t=t||{})this[e]||this.modules[e]?g(e+" Module already exists","error"):this.modules[e]=t[e];return this},n.prototype.router=n.prototype.hash=function(t){var o={path:[],search:{},hash:((t=t||location.hash).match(/[^#](#.*$)/)||[])[1]||""};return/^#\//.test(t)&&(t=t.replace(/^#\//,""),o.href="/"+t,t=t.replace(/([^#])(#.*$)/,"$1").split("/")||[],this.each(t,function(t,e){/^\w+=/.test(e)?(e=e.split("="),o.search[e[0]]=e[1]):o.path.push(e)})),o},n.prototype.url=function(t){var r,e,o=this;return{pathname:(t?((t.match(/\.[^.]+?\/.+/)||[])[0]||"").replace(/^[^\/]+/,"").replace(/\?.+/,""):location.pathname).replace(/^\//,"").split("/"),search:(r={},e=(t?((t.match(/\?.+/)||[])[0]||"").replace(/\#.+/,""):location.search).replace(/^\?+/,"").split("&"),o.each(e,function(t,e){var o=e.indexOf("="),n=o<0?e.substr(0,e.length):0!==o&&e.substr(0,o);n&&(r[n]=0(s.innerHeight||h.documentElement.clientHeight)},d.getStyleRules=function(t,n){if(t)return t=(t=t.sheet||t.styleSheet||{}).cssRules||t.rules,"function"==typeof n&&layui.each(t,function(t,e){if(n(e,t))return!0}),t},d.style=function(t){t=t||{};var e=d.elem("style"),n=t.text||"",i=t.target;if(n)return"styleSheet"in e?(e.setAttribute("type","text/css"),e.styleSheet.cssText=n):e.innerHTML=n,e.id="LAY-STYLE-"+(t.id||(n=d.style.index||0,d.style.index++,"DF-"+n)),i&&((t=d(i).find("#"+e.id))[0]&&t.remove(),d(i).append(e)),e},d.position=function(t,e,n){var i,r,o,c,u,a,f,l;e&&(n=n||{},t!==h&&t!==d("body")[0]||(n.clickType="right"),i="right"===n.clickType?{left:(i=n.e||s.event||{}).clientX,top:i.clientY,right:i.clientX,bottom:i.clientY}:t.getBoundingClientRect(),f=e.offsetWidth,l=e.offsetHeight,r=function(t){return h.body[t=t?"scrollLeft":"scrollTop"]|h.documentElement[t]},o=function(t){return h.documentElement[t?"clientWidth":"clientHeight"]},c="margin"in n?n.margin:5,u=i.left,a=i.bottom,"center"===n.align?u-=(f-t.offsetWidth)/2:"right"===n.align&&(u=u-f+t.offsetWidth),(u=u+f+c>o("width")?o("width")-f-c:u)o()&&(i.top>l+c&&i.top<=o()?a=i.top-l-2*c:n.allowBottomOut||(a=o()-l-2*c)<0&&(a=0)),(f=n.position)&&(e.style.position=f),e.style.left=u+("fixed"===f?0:r(1))+"px",e.style.top=a+("fixed"===f?0:r())+"px",d.hasScrollbar()||(l=e.getBoundingClientRect(),!n.SYSTEM_RELOAD&&l.bottom+c>o()&&(n.SYSTEM_RELOAD=!0,setTimeout(function(){d.position(t,e,n)},50))))},d.options=function(t,e){if(e="object"==typeof e?e:{attr:e},t===h)return{};var t=d(t),n=e.attr||"lay-options",t=t.attr(n);try{return new Function("return "+(t||"{}"))()}catch(i){return layui.hint().error(e.errorText||[n+'="'+t+'"',"\n parseerror: "+i].join("\n"),"error"),{}}},d.isTopElem=function(n){var t=[h,d("body")[0]],i=!1;return d.each(t,function(t,e){if(e===n)return i=!0}),i},d.clipboard={writeText:function(t){var e=String(t.text);try{navigator.clipboard.writeText(e).then(t.done)["catch"](t.error)}catch(i){var n=h.createElement("textarea");n.value=e,n.style.position="fixed",n.style.opacity="0",n.style.top="0px",n.style.left="0px",h.body.appendChild(n),n.select();try{h.execCommand("copy"),"function"==typeof t.done&&t.done()}catch(r){"function"==typeof t.error&&t.error(r)}finally{n.remove?n.remove():h.body.removeChild(n)}}}},r.addStr=function(n,t){return n=n.replace(/\s+/," "),t=t.replace(/\s+/," ").split(" "),d.each(t,function(t,e){new RegExp("\\b"+e+"\\b").test(n)||(n=n+" "+e)}),n.replace(/^\s|\s$/,"")},r.removeStr=function(n,t){return n=n.replace(/\s+/," "),t=t.replace(/\s+/," ").split(" "),d.each(t,function(t,e){e=new RegExp("\\b"+e+"\\b");e.test(n)&&(n=n.replace(e,""))}),n.replace(/\s+/," ").replace(/^\s|\s$/,"")},r.fn.find=function(n){var i=[],r="object"==typeof n;return this.each(function(t,e){e=r&&e.contains(n)?n:e.querySelectorAll(n||null);d.each(e,function(t,e){i.push(e)})}),d(i)},r.fn.each=function(t){return d.each.call(this,this,t)},r.fn.addClass=function(n,i){return this.each(function(t,e){e.className=r[i?"removeStr":"addStr"](e.className,n)})},r.fn.removeClass=function(t){return this.addClass(t,!0)},r.fn.hasClass=function(n){var i=!1;return this.each(function(t,e){new RegExp("\\b"+n+"\\b").test(e.className)&&(i=!0)}),i},r.fn.css=function(e,i){var t=this,r=function(t){return isNaN(t)?t:t+"px"};return"string"!=typeof e||i!==undefined?t.each(function(t,n){"object"==typeof e?d.each(e,function(t,e){n.style[t]=r(e)}):n.style[e]=r(i)}):0]|&(?=#[a-zA-Z0-9]+)/g.test(e+="")?e.replace(/&(?!#?[a-zA-Z0-9]+;)/g,"&").replace(//g,">").replace(/'/g,"'").replace(/"/g,"""):e}},i=function(e){return new RegExp(e,"g")},u=function(e,r){var n="Laytpl Error: ";return"object"==typeof console&&console.error(n+e+"\n"+(r||"")),n+e},n=function(e,r){var n=this,e=(n.config=n.config||{},n.template=e,function(e){for(var r in e)n.config[r]=e[r]});e(c),e(r)},r=(n.prototype.tagExp=function(e,r,n){var c=this.config;return i((r||"")+c.open+["#([\\s\\S])+?","([^{#}])*?"][e||0]+c.close+(n||""))},n.prototype.parse=function(e,r){var n=this,c=n.config,t=e,o=i("^"+c.open+"#",""),p=i(c.close+"$","");if("string"!=typeof e)return e;e='"use strict";var view = "'+(e=e.replace(/\s+|\r|\t|\n/g," ").replace(i(c.open+"#"),c.open+"# ").replace(i(c.close+"}"),"} "+c.close).replace(/\\/g,"\\\\").replace(i(c.open+"!(.+?)!"+c.close),function(e){return e=e.replace(i("^"+c.open+"!"),"").replace(i("!"+c.close),"").replace(i(c.open+"|"+c.close),function(e){return e.replace(/(.)/g,"\\$1")})}).replace(/(?="|')/g,"\\").replace(n.tagExp(),function(e){return'";'+(e=e.replace(o,"").replace(p,"")).replace(/\\(.)/g,"$1")+';view+="'}).replace(n.tagExp(1),function(e){var r='"+laytpl.escape(';return e.replace(/\s/g,"")===c.open+c.close?"":(e=e.replace(i(c.open+"|"+c.close),""),/^=/.test(e)?e=e.replace(/^=/,""):/^-/.test(e)&&(e=e.replace(/^-/,""),r='"+('),r+e.replace(/\\(.)/g,"$1")+')+"')}))+'";return view;';try{return n.cache=e=new Function("d, laytpl",e),e(r,l)}catch(a){return delete n.cache,u(a,t)}},n.prototype.render=function(e,r){e=e||{};var n=this,e=n.cache?n.cache(e,l):n.parse(n.template,e);return"function"==typeof r&&r(e),e},function(e,r){return new n(e,r)});r.config=function(e){for(var r in e=e||{})c[r]=e[r]},r.v="2.0.0",e("laytpl",r)});layui.define(function(e){"use strict";var r=document,u="getElementById",c="getElementsByTagName",a="layui-disabled",t=function(e){var a=this;a.config=e||{},a.config.index=++o.index,a.render(!0)},o=(t.prototype.type=function(){var e=this.config;if("object"==typeof e.elem)return e.elem.length===undefined?2:3},t.prototype.view=function(){var e,i,t,n=this.config,r=n.groups="groups"in n?Number(n.groups)||0:5,u=(n.layout="object"==typeof n.layout?n.layout:["prev","page","next"],n.count=Number(n.count)||0,n.curr=Number(n.curr)||1,n.limits="object"==typeof n.limits?n.limits:[10,20,30,40,50],n.limit=Number(n.limit)||10,n.pages=Math.ceil(n.count/n.limit)||1,n.curr>n.pages?n.curr=n.pages:n.curr<1&&(n.curr=1),r<0?r=1:r>n.pages&&(r=n.pages),n.prev="prev"in n?n.prev:"上一页",n.next="next"in n?n.next:"下一页",n.pages>r?Math.ceil((n.curr+(1'+n.prev+"":"",page:function(){var e=[];if(n.count<1)return"";1'+(n.first||1)+"");var a=Math.floor((r-1)/2),t=1n.pages?n.pages:a:r;for(i-t…');t<=i;t++)t===n.curr?e.push('"+t+""):e.push(''+t+"");return n.pages>r&&n.pages>i&&!1!==n.last&&(i+1…'),0!==r)&&e.push(''+(n.last||n.pages)+""),e.join("")}(),next:n.next?''+n.next+"":"",count:'\u5171 '+n.count+" \u6761",limit:(i=['"),refresh:['','',""].join(""),skip:[''+(e="object"==typeof n.skipText?n.skipText:["到第","页","确定"])[0],'',e[1]+'",""].join("")};return['

',(t=[],layui.each(n.layout,function(e,a){l[a]&&t.push(l[a])}),t.join("")),"
"].join("")},t.prototype.jump=function(e,a){if(e){var t=this,i=t.config,n=e.children,r=e[c]("button")[0],u=e[c]("input")[0],e=e[c]("select")[0],l=function(){var e=Number(u.value.replace(/\s|\D/g,""));e&&(i.curr=e,t.render())};if(a)return l();for(var s=0,p=n.length;si.pages||(i.curr=e,t.render())});e&&o.on(e,"change",function(){var e=this.value;i.curr*e>i.count&&(i.curr=Math.ceil(i.count/e)),i.limit=e,t.render()}),r&&o.on(r,"click",function(){l()})}},t.prototype.skip=function(t){var i,e;t&&(i=this,e=t[c]("input")[0])&&o.on(e,"keyup",function(e){var a=this.value,e=e.keyCode;/^(37|38|39|40)$/.test(e)||(/\D/.test(a)&&(this.value=a.replace(/\D/,"")),13===e&&i.jump(t,!0))})},t.prototype.render=function(e){var a=this,t=a.config,i=a.type(),n=a.view(),i=(2===i?t.elem&&(t.elem.innerHTML=n):3===i?t.elem.html(n):r[u](t.elem)&&(r[u](t.elem).innerHTML=n),t.jump&&t.jump(t,e),r[u]("layui-laypage-"+t.index));a.jump(i),t.hash&&!e&&(location.hash="!"+t.hash+"="+t.curr),a.skip(i)},{render:function(e){return new t(e).index},index:layui.laypage?layui.laypage.index+1e4:0,on:function(a,e,t){return a.attachEvent?a.attachEvent("on"+e,function(e){e.target=e.srcElement,t.call(a,e)}):a.addEventListener(e,t,!1),this}});e("laypage",o)});!function(i,v){"use strict";var n=i.layui&&layui.define,l={getPath:i.lay&&lay.getPath?lay.getPath:"",link:function(e,t,a){D.path&&i.lay&&lay.layui&&lay.layui.link(D.path+e,t,a)}},e=i.LAYUI_GLOBAL||{},d="layui-laydate-id",D={v:"5.5.0",config:{weekStart:0},index:i.laydate&&i.laydate.v?1e5:0,path:e.laydate_dir||l.getPath,set:function(e){var t=this;return t.config=lay.extend({},t.config,e),t},ready:function(e){var t="laydate",a=(n?"modules/":"")+"laydate.css?v="+D.v;return n?layui["layui.all"]?"function"==typeof e&&e():layui.addcss(a,e,t):l.link(a,e,t),this}},s=function(){var t=this,e=t.config.id;return(s.that[e]=t).inst={hint:function(e){t.hint.call(t,e)},reload:function(e){t.reload.call(t,e)},config:t.config}},a="laydate",x="layui-this",k="laydate-disabled",h=[100,2e5],T="layui-laydate-static",w="layui-laydate-list",o="laydate-selected",r="layui-laydate-hint",y="laydate-day-prev",m="laydate-day-next",C=".laydate-btns-confirm",M="laydate-time-text",L="laydate-btns-time",E="layui-laydate-preview",S="layui-laydate-shade",I=function(e){var t,a=this,n=(a.index=++D.index,a.config=lay.extend({},a.config,D.config,e),lay(e.elem||a.config.elem));return 1\u8bf7\u91cd\u65b0\u9009\u62e9",invalidDate:"\u4e0d\u5728\u6709\u6548\u65e5\u671f\u6216\u65f6\u95f4\u8303\u56f4\u5185",formatError:["\u65e5\u671f\u683c\u5f0f\u4e0d\u5408\u6cd5
\u5fc5\u987b\u9075\u5faa\u4e0b\u8ff0\u683c\u5f0f\uff1a
","
\u5df2\u4e3a\u4f60\u91cd\u7f6e"],preview:"\u5f53\u524d\u9009\u4e2d\u7684\u7ed3\u679c"},en:{weeks:["Su","Mo","Tu","We","Th","Fr","Sa"],time:["Hours","Minutes","Seconds"],timeTips:"Select Time",startTime:"Start Time",endTime:"End Time",dateTips:"Select Date",month:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],tools:{confirm:"Confirm",clear:"Clear",now:"Now"},timeout:"End time cannot be less than start Time
Please re-select",invalidDate:"Invalid date",formatError:["The date format error
Must be followed\uff1a
","
It has been reset"],preview:"The selected result"}};return e[this.config.lang]||e.cn},I.prototype.reload=function(e){this.config=lay.extend({},this.config,e),this.init()},I.prototype.init=function(){var r=this,o=r.config,e="static"===o.position,t={year:"yyyy",month:"yyyy-MM",date:"yyyy-MM-dd",time:"HH:mm:ss",datetime:"yyyy-MM-dd HH:mm:ss"};o.elem=lay(o.elem),o.eventElem=lay(o.eventElem),o.elem[0]&&("array"!==layui.type(o.theme)&&(o.theme=[o.theme]),o.fullPanel&&("datetime"!==o.type||o.range)&&delete o.fullPanel,r.rangeStr=o.range?"string"==typeof o.range?o.range:"-":"",r.rangeLinked=!(!o.range||!o.rangeLinked||"date"!==o.type&&"datetime"!==o.type),r.autoCalendarModel=function(){var e=r.rangeLinked;return r.rangeLinked=o.range&&("date"===o.type||"datetime"===o.type)&&(!r.startDate||!r.endDate||r.startDate&&r.endDate&&r.startDate.year===r.endDate.year&&r.startDate.month===r.endDate.month),lay(r.elem)[r.rangeLinked?"addClass":"removeClass"]("layui-laydate-linkage"),r.rangeLinked!=e},r.autoCalendarModel.auto=r.rangeLinked&&"auto"===o.rangeLinked,"array"===layui.type(o.range)&&(r.rangeElem=[lay(o.range[0]),lay(o.range[1])]),t[o.type]||(i.console&&console.error&&console.error("laydate type error:'"+o.type+"' is not supported"),o.type="date"),o.format===t.date&&(o.format=t[o.type]||t.date),r.format=s.formatArr(o.format),o.weekStart&&!/^[0-6]$/.test(o.weekStart)&&(t=r.lang(),o.weekStart=t.weeks.indexOf(o.weekStart),-1===o.weekStart)&&(o.weekStart=0),r.EXP_IF="",r.EXP_SPLIT="",lay.each(r.format,function(e,t){e=new RegExp(c).test(t)?"\\d{"+(new RegExp(c).test(r.format[0===e?e+1:e-1]||"")?/^yyyy|y$/.test(t)?4:t.length:/^yyyy$/.test(t)?"1,4":/^y$/.test(t)?"1,308":"1,2")+"}":"\\"+t;r.EXP_IF=r.EXP_IF+e,r.EXP_SPLIT=r.EXP_SPLIT+"("+e+")"}),r.EXP_IF_ONE=new RegExp("^"+r.EXP_IF+"$"),r.EXP_IF=new RegExp("^"+(o.range?r.EXP_IF+"\\s\\"+r.rangeStr+"\\s"+r.EXP_IF:r.EXP_IF)+"$"),r.EXP_SPLIT=new RegExp("^"+r.EXP_SPLIT+"$",""),r.isInput(o.elem[0])||"focus"===o.trigger&&(o.trigger="click"),o.elem.attr("lay-key",r.index),o.eventElem.attr("lay-key",r.index),o.elem.attr(d,o.id),o.mark=lay.extend({},o.calendar&&"cn"===o.lang?{"0-1-1":"\u5143\u65e6","0-2-14":"\u60c5\u4eba","0-3-8":"\u5987\u5973","0-3-12":"\u690d\u6811","0-4-1":"\u611a\u4eba","0-5-1":"\u52b3\u52a8","0-5-4":"\u9752\u5e74","0-6-1":"\u513f\u7ae5","0-9-10":"\u6559\u5e08","0-10-1":"\u56fd\u5e86","0-12-25":"\u5723\u8bde"}:{},o.mark),lay.each(["min","max"],function(e,t){var a=[],n=[];if("number"==typeof o[t])var i=o[t],l=new Date,l=r.newDate({year:l.getFullYear(),month:l.getMonth(),date:l.getDate(),hours:e?23:0,minutes:e?59:0,seconds:e?59:0}).getTime(),e=new Date(i?i<864e5?l+864e5*i:i:l),a=[e.getFullYear(),e.getMonth()+1,e.getDate()],n=[e.getHours(),e.getMinutes(),e.getSeconds()];else if("string"==typeof o[t])a=(o[t].match(/\d+-\d+-\d+/)||[""])[0].split("-"),n=(o[t].match(/\d+:\d+:\d+/)||[""])[0].split(":");else if("object"==typeof o[t])return o[t];o[t]={year:0|a[0]||(new Date).getFullYear(),month:a[1]?(0|a[1])-1:(new Date).getMonth(),date:0|a[2]||(new Date).getDate(),hours:0|n[0],minutes:0|n[1],seconds:0|n[2]}}),r.elemID="layui-laydate"+o.elem.attr("lay-key"),(o.show||e)&&r.render(),e||r.events(),o.value)&&o.isInitValue&&("date"===layui.type(o.value)?r.setValue(r.parse(0,r.systemDate(o.value))):r.setValue(o.value))},I.prototype.render=function(){var a,n,i,l,r=this,o=r.config,d=r.lang(),s="static"===o.position,y=r.elem=lay.elem("div",{id:r.elemID,"class":["layui-laydate",o.range?" layui-laydate-range":"",r.rangeLinked?" layui-laydate-linkage":"",s?" "+T:"",o.fullPanel?" laydate-theme-fullpanel":"",(a="",lay.each(o.theme,function(e,t){"default"===t||/^#/.test(t)||(a+=" laydate-theme-"+t)}),a)].join("")}),m=r.elemMain=[],c=r.elemHeader=[],u=r.elemCont=[],h=r.table=[],e=r.footer=lay.elem("div",{"class":"layui-laydate-footer"}),t=r.shortcut=lay.elem("ul",{"class":"layui-laydate-shortcut"}),f=(o.zIndex&&(y.style.zIndex=o.zIndex),lay.each(new Array(2),function(e){if(!o.range&&0'+d.timeTips+""),(o.range||"datetime"!==o.type||o.fullPanel)&&f.push(''),lay.each(o.btns,function(e,t){var a=d.tools[t]||"btn";o.range&&"now"===t||(s&&"clear"===t&&(a="cn"===o.lang?"\u91cd\u7f6e":"Reset"),n.push(''+a+""))}),f.push('"),f.join(""))),o.shortcuts&&(y.appendChild(t),lay(t).html((i=[],lay.each(o.shortcuts,function(e,t){i.push('
  • '+t.text+"
  • ")}),i.join(""))).find("li").on("click",function(e){var t=o.shortcuts[this.dataset.index]||{},t=("function"==typeof t.value?t.value():t.value)||[],n=(layui.isArray(t)||(t=[t]),o.type),t=(lay.each(t,function(e,t){var a=[o.dateTime,r.endDate][e];"time"===n&&"date"!==layui.type(t)?r.EXP_IF.test(t)&&(t=(t.match(r.EXP_SPLIT)||[]).slice(1),lay.extend(a,{hours:0|t[0],minutes:0|t[2],seconds:0|t[4]})):lay.extend(a,r.systemDate("date"===layui.type(t)?t:new Date(t))),"time"!==n&&"datetime"!==n||(r[["startTime","endTime"][e]]={hours:a.hours,minutes:a.minutes,seconds:a.seconds}),0===e?r.startDate=lay.extend({},a):r.endState=!0,"year"===n||"month"===n||"time"===n?r.listYM[e]=[a.year,a.month+1]:e&&r.autoCalendarModel.auto&&r.autoCalendarModel()}),r.checkDate("limit").calendar(null,null,"init"),lay(r.footer).find("."+L).removeClass(k));t&&"date"===t.attr("lay-type")&&t[0].click(),r.done(null,"change"),lay(this).addClass(x),"static"!==o.position&&r.setValue(r.parse()).done().remove()})),lay.each(m,function(e,t){y.appendChild(t)}),o.showBottom&&y.appendChild(e),lay.elem("style")),p=[],g=!0,t=(lay.each(o.theme,function(e,t){g&&/^#/.test(t)?(g=!(l=!0),p.push(["#{{id}} .layui-laydate-header{background-color:{{theme}};}","#{{id}} li.layui-this,#{{id}} td.layui-this>div{background-color:{{theme}} !important;}",-1!==o.theme.indexOf("circle")?"":"#{{id}} .layui-this{background-color:{{theme}} !important;}","#{{id}} .laydate-day-now{color:{{theme}} !important;}","#{{id}} .laydate-day-now:after{border-color:{{theme}} !important;}"].join("").replace(/{{id}}/g,r.elemID).replace(/{{theme}}/g,t))):!g&&/^#/.test(t)&&p.push(["#{{id}} .laydate-selected>div{background-color:{{theme}} !important;}","#{{id}} .laydate-selected:hover>div{background-color:{{theme}} !important;}"].join("").replace(/{{id}}/g,r.elemID).replace(/{{theme}}/g,t))}),o.shortcuts&&o.range&&p.push("#{{id}}.layui-laydate-range{width: 628px;}".replace(/{{id}}/g,r.elemID)),p.length&&(p=p.join(""),"styleSheet"in f?(f.setAttribute("type","text/css"),f.styleSheet.cssText=p):f.innerHTML=p,l&&lay(y).addClass("laydate-theme-molv"),y.appendChild(f)),r.remove(I.thisElemDate),D.thisId=o.id,s?o.elem.append(y):(v.body.appendChild(y),r.position()),o.shade?'
    ':"");y.insertAdjacentHTML("beforebegin",t),r.checkDate().calendar(null,0,"init"),r.changeEvent(),I.thisElemDate=r.elemID,r.renderAdditional(),"function"==typeof o.ready&&o.ready(lay.extend({},o.dateTime,{month:o.dateTime.month+1})),r.preview()},I.prototype.remove=function(e){var t=this,a=t.config,n=lay("#"+(e||t.elemID));return n[0]&&(n.hasClass(T)||t.checkDate(function(){n.remove(),delete t.startDate,delete t.endDate,delete t.endState,delete t.startTime,delete t.endTime,delete D.thisId,"function"==typeof a.close&&a.close(t)}),lay("."+S).remove()),t},I.prototype.position=function(){var e=this.config;return lay.position(e.elem[0],this.elem,{position:e.position}),this},I.prototype.hint=function(e){var t=this,a=(t.config,lay.elem("div",{"class":r}));t.elem&&(a.innerHTML=(e="object"==typeof e?e||{}:{content:e}).content||"",lay(t.elem).find("."+r).remove(),t.elem.appendChild(a),clearTimeout(t.hinTimer),t.hinTimer=setTimeout(function(){lay(t.elem).find("."+r).remove()},"ms"in e?e.ms:3e3))},I.prototype.getAsYM=function(e,t,a){return a?t--:t++,t<0&&(t=11,e--),11h[1]&&(e.year=h[1],o=!0),11t)&&(e.date=t,o=!0)},u=function(n,i,l){var r=["startTime","endTime"];i=(i.match(d.EXP_SPLIT)||[]).slice(1),l=l||0,s.range&&(d[r[l]]=d[r[l]]||{}),lay.each(d.format,function(e,t){var a=parseFloat(i[e]);i[e].lengthd.getDateTime(s.max)?(r=s.dateTime=lay.extend({},s.max),i=!0):d.getDateTime(r)d.getDateTime(s.max))&&(d.endDate=lay.extend({},s.max),i=!0),d.startTime={hours:s.dateTime.hours,minutes:s.dateTime.minutes,seconds:s.dateTime.seconds},d.endTime={hours:d.endDate.hours,minutes:d.endDate.minutes,seconds:d.endDate.seconds},"month"===s.type)&&(s.dateTime.date=1,d.endDate.date=1),i&&m&&(d.setValue(d.parse()),d.hint("value "+l.invalidDate+l.formatError[1])),d.startDate=d.startDate||m&&lay.extend({},s.dateTime),d.autoCalendarModel.auto&&d.autoCalendarModel(),d.endState=!s.range||!d.rangeLinked||!(!d.startDate||!d.endDate),e&&e()),d},I.prototype.mark=function(e,a){var n,t=this.config;return lay.each(t.mark,function(e,t){e=e.split("-");e[0]!=a[0]&&0!=e[0]||e[1]!=a[1]&&0!=e[1]||e[2]!=a[2]||(n=t||a[2])}),n&&e.find("div").html(''+n+""),this},I.prototype.holidays=function(n,i){var e=this.config,l=["","work"];return"array"===layui.type(e.holidays)&&lay.each(e.holidays,function(a,e){lay.each(e,function(e,t){t===n.attr("lay-ymd")&&n.find("div").html('"+i[2]+"")})}),this},I.prototype.limit=function(t){t=t||{};var i=this,e=i.config,l={},a=t.index>(t.time?0:41)?i.endDate:e.dateTime;return lay.each({now:lay.extend({},a,t.date||{}),min:e.min,max:e.max},function(e,a){var n;l[e]=i.newDate(lay.extend({year:a.year,month:"year"===t.type?0:a.month,date:"year"===t.type||"month"===t.type?1:a.date},(n={},lay.each(t.time,function(e,t){n[t]=a[t]}),n))).getTime()}),a=l.nowh[1]&&(d.year=h[1],o.hint(y.invalidDate)),o.firstDate||(o.firstDate=lay.extend({},d)),s.setFullYear(d.year,d.month,1),i=(s.getDay()+(7-n.weekStart))%7,l=D.getEndDate(d.month||12,d.year),r=D.getEndDate(d.month+1,d.year),lay.each(c,function(e,t){var a,n=[d.year,d.month];(t=lay(t)).removeAttr("class"),e"+n[2]+""),o.mark(t,n).holidays(t,n).limit({elem:t,date:{year:n[0],month:n[1]-1,date:n[2]},index:e})}),lay(u[0]).attr("lay-ym",d.year+"-"+(d.month+1)),lay(u[1]).attr("lay-ym",d.year+"-"+(d.month+1)),"cn"===n.lang?(lay(u[0]).attr("lay-type","year").html(d.year+" \u5e74"),lay(u[1]).attr("lay-type","month").html(d.month+1+" \u6708")):(lay(u[0]).attr("lay-type","month").html(y.month[d.month]),lay(u[1]).attr("lay-type","year").html(d.year)),m&&(n.range?!e&&"init"===a||(o.listYM=[[(o.startDate||n.dateTime).year,(o.startDate||n.dateTime).month+1],[o.endDate.year,o.endDate.month+1]],o.list(n.type,0).list(n.type,1),"time"===n.type?o.setBtnStatus("\u65f6\u95f4",lay.extend({},o.systemDate(),o.startTime),lay.extend({},o.systemDate(),o.endTime)):o.setBtnStatus(!0)):(o.listYM=[[d.year,d.month+1]],o.list(n.type,0))),n.range&&"init"===a&&(o.rangeLinked?(s=o.getAsYM(d.year,d.month,t?"sub":null),o.calendar(lay.extend({},d,{year:s[0],month:s[1]}),1-t)):o.calendar(null,1-t)),n.range||(c=["hours","minutes","seconds"],o.limit({elem:lay(o.footer).find(".laydate-btns-now"),date:o.systemDate(/^(datetime|time)$/.test(n.type)?new Date:null),index:0,time:c}),o.limit({elem:lay(o.footer).find(C),index:0,time:c})),o.setBtnStatus(),lay(o.shortcut).find("li."+x).removeClass(x),n.range&&!m&&"init"!==a&&o.stampRange(),o},I.prototype.list=function(n,i){var l,r,e,o,d=this,s=d.config,y=d.rangeLinked?s.dateTime:[s.dateTime,d.endDate][i],m=d.lang(),t=s.range&&"date"!==s.type&&"datetime"!==s.type,c=lay.elem("ul",{"class":w+" "+{year:"laydate-year-list",month:"laydate-month-list",time:"laydate-time-list"}[n]}),a=d.elemHeader[i],u=lay(a[2]).find("span"),h=d.elemCont[i||0],f=lay(h).find("."+w)[0],p="cn"===s.lang,g=p?"\u5e74":"",v=d.listYM[i]||{},D=["hours","minutes","seconds"],T=["startTime","endTime"][i];return v[0]<1&&(v[0]=1),"year"===n?(e=l=v[0]-7,l<1&&(e=l=1),lay.each(new Array(15),function(e){var t=lay.elem("li",{"lay-ym":l}),a={year:l,month:0,date:1};l==v[0]&&lay(t).addClass(x),t.innerHTML=l+g,c.appendChild(t),d.limit({elem:lay(t),date:a,index:i,type:n}),l++}),lay(u[p?0:1]).attr("lay-ym",l-8+"-"+v[1]).html(e+g+" - "+(l-1)+g)):"month"===n?(lay.each(new Array(12),function(e){var t=lay.elem("li",{"lay-ym":e}),a={year:v[0],month:e,date:1};e+1==v[1]&&lay(t).addClass(x),t.innerHTML=m.month[e]+(p?"\u6708":""),c.appendChild(t),d.limit({elem:lay(t),date:a,index:i,type:n})}),lay(u[p?0:1]).attr("lay-ym",v[0]+"-"+v[1]).html(v[0]+g)):"time"===n&&(r=function(){lay(c).find("ol").each(function(a,e){lay(e).find("li").each(function(e,t){d.limit({elem:lay(t),date:[{hours:e},{hours:d[T].hours,minutes:e},{hours:d[T].hours,minutes:d[T].minutes,seconds:e}][a],index:i,time:[["hours"],["hours","minutes"],["hours","minutes","seconds"]][a]})})}),s.range||d.limit({elem:lay(d.footer).find(C),date:d[T],inedx:0,time:["hours","minutes","seconds"]})},s.range?d[T]||(d[T]="startTime"===T?y:d.endDate):d[T]=y,lay.each([24,60,60],function(t,e){var a=lay.elem("li"),n=["

    "+m.time[t]+"

      "];lay.each(new Array(e),function(e){n.push(""+lay.digit(e,2)+"")}),a.innerHTML=n.join("")+"
    ",c.appendChild(a)}),r()),f&&h.removeChild(f),h.appendChild(c),"year"===n||"month"===n?(lay(d.elemMain[i]).addClass("laydate-ym-show"),lay(c).find("li").on("click",function(){var e=0|lay(this).attr("lay-ym");lay(this).hasClass(k)||(d.rangeLinked?lay.extend(y,{year:"year"===n?e:v[0],month:"year"===n?v[1]-1:e}):y[n]=e,"year"===s.type||"month"===s.type?(lay(c).find("."+x).removeClass(x),lay(this).addClass(x),"month"===s.type&&"year"===n&&(d.listYM[i][0]=e,t&&((i?d.endDate:y).year=e),d.list("month",i))):(d.checkDate("limit").calendar(y,i,"init"),d.closeList()),d.setBtnStatus(),!s.range&&s.autoConfirm&&("month"===s.type&&"month"===n||"year"===s.type&&"year"===n)&&d.setValue(d.parse()).done().remove(),d.autoCalendarModel.auto&&!d.rangeLinked?d.choose(lay(h).find("td.layui-this"),i):d.endState&&d.done(null,"change"),lay(d.footer).find("."+L).removeClass(k))})):(e=lay.elem("span",{"class":M}),o=function(){lay(c).find("ol").each(function(e){var a=this,t=lay(a).find("li");a.scrollTop=30*(d[T][D[e]]-2),a.scrollTop<=0&&t.each(function(e,t){if(!lay(this).hasClass(k))return a.scrollTop=30*(e-2),!0})})},u=lay(a[2]).find("."+M),o(),e.innerHTML=s.range?[m.startTime,m.endTime][i]:m.timeTips,lay(d.elemMain[i]).addClass("laydate-time-show"),u[0]&&u.remove(),a[2].appendChild(e),lay(c).find("ol").each(function(t){var a=this;lay(a).find("li").on("click",function(){var e=0|this.innerHTML;lay(this).hasClass(k)||(s.range?d[T][D[t]]=e:y[D[t]]=e,lay(a).find("."+x).removeClass(x),lay(this).addClass(x),r(),o(),(d.endDate||"time"===s.type||"datetime"===s.type&&s.fullPanel)&&d.done(null,"change"),d.setBtnStatus())})})),d},I.prototype.listYM=[],I.prototype.closeList=function(){var a=this;a.config;lay.each(a.elemCont,function(e,t){lay(this).find("."+w).remove(),lay(a.elemMain[e]).removeClass("laydate-ym-show laydate-time-show")}),lay(a.elem).find("."+M).remove()},I.prototype.setBtnStatus=function(e,t,a){var n=this,i=n.config,l=n.lang(),r=lay(n.footer).find(C);i.range&&"time"!==i.type&&(t=t||(n.rangeLinked?n.startDate:i.dateTime),a=a||n.endDate,i=!n.endState||n.newDate(t).getTime()>n.newDate(a).getTime(),n.limit({date:t})||n.limit({date:a})?r.addClass(k):r[i?"addClass":"removeClass"](k),e)&&i&&n.hint("string"==typeof e?l.timeout.replace(/\u65e5\u671f/g,e):l.timeout)},I.prototype.parse=function(e,t){var a=this,n=a.config,i=a.rangeLinked?a.startDate:n.dateTime,t=t||("end"==e?lay.extend({},a.endDate,a.endTime):n.range?lay.extend({},i||n.dateTime,a.startTime):n.dateTime),i=D.parse(t,a.format,1);return n.range&&e===undefined?i+" "+a.rangeStr+" "+a.parse("end"):i},I.prototype.newDate=function(e){return e=e||{},new Date(e.year||1,e.month||0,e.date||1,e.hours||0,e.minutes||0,e.seconds||0)},I.prototype.getDateTime=function(e){return this.newDate(e).getTime()},I.prototype.setValue=function(e){var t=this,a=t.config,n=a.elem[0];return"static"!==a.position&&(e=e||"",t.isInput(n)?lay(n).val(e):(a=t.rangeElem)?("array"!==layui.type(e)&&(e=e.split(" "+t.rangeStr+" ")),a[0].val(e[0]||""),a[1].val(e[1]||"")):(0===lay(n).find("*").length&&lay(n).html(e),lay(n).attr("lay-date",e))),t},I.prototype.preview=function(){var e,t=this,a=t.config;a.isPreview&&(e=lay(t.elem).find("."+E),a=!a.range||(t.rangeLinked?t.endState:t.endDate)?t.parse():"",e.html(a),e.html())&&(e.css({color:"#16b777"}),setTimeout(function(){e.css({color:"#777"})},300))},I.prototype.renderAdditional=function(){this.config.fullPanel&&this.list("time",0)},I.prototype.stampRange=function(){var n,i=this,l=i.config,r=i.rangeLinked?i.startDate:l.dateTime,e=lay(i.elem).find("td");l.range&&!i.endState&&lay(i.footer).find(C).addClass(k),r=r&&i.newDate({year:r.year,month:r.month,date:r.date}).getTime(),n=i.endState&&i.endDate&&i.newDate({year:i.endDate.year,month:i.endDate.month,date:i.endDate.date}).getTime(),lay.each(e,function(e,t){var a=lay(t).attr("lay-ymd").split("-"),a=i.newDate({year:a[0],month:a[1]-1,date:a[2]}).getTime();l.rangeLinked&&!i.startDate&&a===i.newDate(i.systemDate()).getTime()&&lay(t).addClass(lay(t).hasClass(y)||lay(t).hasClass(m)?"":"laydate-day-now"),lay(t).removeClass(o+" "+x),a!==r&&a!==n||(i.rangeLinked||!i.rangeLinked&&(e<42?a===r:a===n))&&lay(t).addClass(lay(t).hasClass(y)||lay(t).hasClass(m)?o:x),rn.getDateTime(i.max)&&(n[t]={hours:i.max.hours,minutes:i.max.minutes,seconds:i.max.seconds},lay.extend(l,n[t])))}),a||(n.startDate=lay.extend({},l)),n.endState&&!n.limit({date:n.thisDateTime(1-a)})&&(((r=n.endState&&n.autoCalendarModel.auto?n.autoCalendarModel():r)||n.rangeLinked&&n.endState)&&n.newDate(n.startDate)>n.newDate(n.endDate)&&(e=n.startDate.year===n.endDate.year&&n.startDate.month===n.endDate.month&&n.startDate.date===n.endDate.date,o=n.startDate,n.startDate=lay.extend({},n.endDate,e?{}:n.startTime),i.dateTime=lay.extend({},n.startDate),n.endDate=lay.extend({},o,e?{}:n.endTime),e)&&(o=n.startTime,n.startTime=n.endTime,n.endTime=o),r)&&(i.dateTime=lay.extend({},n.startDate)),n.rangeLinked?(e=lay.extend({},l),!t||a||r||(o=n.getAsYM(l.year,l.month,"sub"),lay.extend(i.dateTime,{year:o[0],month:o[1]})),n.calendar(e,t,r?"init":null)):n.calendar(null,a,r?"init":null),n.endState&&n.done(null,"change")):"static"===i.position?n.calendar().done().done(null,"change"):"date"===i.type?i.autoConfirm?n.setValue(n.parse()).done().remove():n.calendar().done(null,"change"):"datetime"===i.type&&n.calendar().done(null,"change"))},I.prototype.tool=function(t,e){var a=this,n=a.config,i=a.lang(),l=n.dateTime,r="static"===n.position,o={datetime:function(){lay(t).hasClass(k)||(a.list("time",0),n.range&&a.list("time",1),lay(t).attr("lay-type","date").html(a.lang().dateTips))},date:function(){a.closeList(),lay(t).attr("lay-type","datetime").html(a.lang().timeTips)},clear:function(){r&&(lay.extend(l,a.firstDate),a.calendar()),n.range&&(delete n.dateTime,delete a.endDate,delete a.startTime,delete a.endTime),a.setValue(""),a.done(null,"onClear").done(["",{},{}]).remove()},now:function(){var e=new Date;if(lay(t).hasClass(k))return a.hint(i.tools.now+", "+i.invalidDate);lay.extend(l,a.systemDate(),{hours:e.getHours(),minutes:e.getMinutes(),seconds:e.getSeconds()}),a.setValue(a.parse()),r&&a.calendar(),a.done(null,"onNow").done().remove()},confirm:function(){if(n.range){if(lay(t).hasClass(k))return a.hint("time"===n.type?i.timeout.replace(/\u65e5\u671f/g,"\u65f6\u95f4"):i.timeout)}else if(lay(t).hasClass(k))return a.hint(i.invalidDate);a.setValue(a.parse()),a.done(null,"onConfirm").done().remove()}};o[e]&&o[e]()},I.prototype.change=function(n){var i=this,l=i.config,r=i.thisDateTime(n),o=l.range&&("year"===l.type||"month"===l.type),d=i.elemCont[n||0],s=i.listYM[n],e=function(e){var t=lay(d).find(".laydate-year-list")[0],a=lay(d).find(".laydate-month-list")[0];return t&&(s[0]=e?s[0]-15:s[0]+15,i.list("year",n)),a&&(e?s[0]--:s[0]++,i.list("month",n)),(t||a)&&(lay.extend(r,{year:s[0]}),o&&(r.year=s[0]),l.range||i.done(null,"change"),l.range||i.limit({elem:lay(i.footer).find(C),date:{year:s[0]}})),i.setBtnStatus(),t||a};return{prevYear:function(){e("sub")||(i.rangeLinked?(l.dateTime.year--,i.checkDate("limit").calendar(null,null,"init")):(r.year--,i.checkDate("limit").calendar(null,n),i.autoCalendarModel.auto?i.choose(lay(d).find("td.layui-this"),n):i.done(null,"change")))},prevMonth:function(){i.rangeLinked&&(r=l.dateTime);var e=i.getAsYM(r.year,r.month,"sub");lay.extend(r,{year:e[0],month:e[1]}),i.checkDate("limit").calendar(null,null,"init"),i.rangeLinked||(i.autoCalendarModel.auto?i.choose(lay(d).find("td.layui-this"),n):i.done(null,"change"))},nextMonth:function(){i.rangeLinked&&(r=l.dateTime);var e=i.getAsYM(r.year,r.month);lay.extend(r,{year:e[0],month:e[1]}),i.checkDate("limit").calendar(null,null,"init"),i.rangeLinked||(i.autoCalendarModel.auto?i.choose(lay(d).find("td.layui-this"),n):i.done(null,"change"))},nextYear:function(){e()||(i.rangeLinked?(l.dateTime.year++,i.checkDate("limit").calendar(null,0,"init")):(r.year++,i.checkDate("limit").calendar(null,n),i.autoCalendarModel.auto?i.choose(lay(d).find("td.layui-this"),n):i.done(null,"change")))}}},I.prototype.changeEvent=function(){var i=this;i.config;lay(i.elem).on("click",function(e){lay.stope(e)}).on("mousedown",function(e){lay.stope(e)}),lay.each(i.elemHeader,function(n,e){lay(e[0]).on("click",function(e){i.change(n).prevYear()}),lay(e[1]).on("click",function(e){i.change(n).prevMonth()}),lay(e[2]).find("span").on("click",function(e){var t=lay(this),a=t.attr("lay-ym"),t=t.attr("lay-type");a&&(a=a.split("-"),i.listYM[n]=[0|a[0],0|a[1]],i.list(t,n),lay(i.footer).find("."+L).addClass(k))}),lay(e[3]).on("click",function(e){i.change(n).nextMonth()}),lay(e[4]).on("click",function(e){i.change(n).nextYear()})}),lay.each(i.table,function(e,t){lay(t).find("td").on("click",function(){i.choose(lay(this),e)})}),lay(i.footer).find("span").on("click",function(){var e=lay(this).attr("lay-type");i.tool(this,e)})},I.prototype.isInput=function(e){return/input|textarea/.test(e.tagName.toLocaleLowerCase())||/INPUT|TEXTAREA/.test(e.tagName)},I.prototype.events=function(){var e,t=this,a=t.config;a.elem[0]&&!a.elem[0].eventHandler&&(a.elem.on(a.trigger,e=function(){D.thisId!==a.id&&t.render()}),a.elem[0].eventHandler=!0,a.eventElem.on(a.trigger,e),t.unbind=function(){t.remove(),a.elem.off(a.trigger,e),a.elem.removeAttr("lay-key"),a.elem.removeAttr(d),a.elem[0].eventHandler=!1,a.eventElem.off(a.trigger,e),a.eventElem.removeAttr("lay-key"),delete s.that[a.id]})},s.that={},s.getThis=function(e){var t=s.that[e];return!t&&n&&layui.hint().error(e?a+" instance with ID '"+e+"' not found":"ID argument required"),t},l.run=function(n){n(v).on("mousedown",function(e){var t,a;D.thisId&&(t=s.getThis(D.thisId))&&(a=t.config,e.target===a.elem[0]||e.target===a.eventElem[0]||e.target===n(a.closeStop)[0]||a.elem[0]&&a.elem[0].contains(e.target)||t.remove())}).on("keydown",function(e){var t;D.thisId&&(t=s.getThis(D.thisId))&&"static"!==t.config.position&&13===e.keyCode&&n("#"+t.elemID)[0]&&t.elemID===I.thisElemDate&&(e.preventDefault(),n(t.footer).find(C)[0].click())}),n(i).on("resize",function(){if(D.thisId){var e=s.getThis(D.thisId);if(e)return!(!e.elem||!n(".layui-laydate")[0])&&void e.position()}})},D.render=function(e){e=new I(e);return s.call(e)},D.reload=function(e,t){e=s.getThis(e);if(e)return e.reload(t)},D.getInst=function(e){e=s.getThis(e);if(e)return e.inst},D.hint=function(e,t){e=s.getThis(e);if(e)return e.hint(t)},D.unbind=function(e){e=s.getThis(e);if(e)return e.unbind()},D.close=function(e){e=s.getThis(e||D.thisId);if(e)return e.remove()},D.parse=function(a,n,i){return a=a||{},n=((n="string"==typeof n?s.formatArr(n):n)||[]).concat(),lay.each(n,function(e,t){/yyyy|y/.test(t)?n[e]=lay.digit(a.year,t.length):/MM|M/.test(t)?n[e]=lay.digit(a.month+(i||0),t.length):/dd|d/.test(t)?n[e]=lay.digit(a.date,t.length):/HH|H/.test(t)?n[e]=lay.digit(a.hours,t.length):/mm|m/.test(t)?n[e]=lay.digit(a.minutes,t.length):/ss|s/.test(t)&&(n[e]=lay.digit(a.seconds,t.length))}),n.join("")},D.getEndDate=function(e,t){var a=new Date;return a.setFullYear(t||a.getFullYear(),e||a.getMonth()+1,1),new Date(a.getTime()-864e5).getDate()},n?(D.ready(),layui.define("lay",function(e){D.path=layui.cache.dir,l.run(lay),e(a,D)})):"function"==typeof define&&define.amd?define(function(){return l.run(lay),D}):(D.ready(),l.run(i.lay),i.laydate=D)}(window,window.document);!function(e,t){"object"==typeof module&&"object"==typeof module.exports?module.exports=e.document?t(e):function(e){if(e.document)return t(e);throw new Error("jQuery requires a window with a document")}:t(e)}("undefined"!=typeof window?window:this,function(T,M){var f=[],g=T.document,c=f.slice,O=f.concat,R=f.push,P=f.indexOf,B={},W=B.toString,m=B.hasOwnProperty,y={},e="1.12.4",C=function(e,t){return new C.fn.init(e,t)},I=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,$=/^-ms-/,z=/-([\da-z])/gi,X=function(e,t){return t.toUpperCase()};function U(e){var t=!!e&&"length"in e&&e.length,n=C.type(e);return"function"!==n&&!C.isWindow(e)&&("array"===n||0===t||"number"==typeof t&&0+~]|"+a+")"+a+"*"),ee=new RegExp("="+a+"*([^\\]'\"]*?)"+a+"*\\]","g"),te=new RegExp(G),ne=new RegExp("^"+s+"$"),f={ID:new RegExp("^#("+s+")"),CLASS:new RegExp("^\\.("+s+")"),TAG:new RegExp("^("+s+"|[*])"),ATTR:new RegExp("^"+J),PSEUDO:new RegExp("^"+G),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+a+"*(even|odd|(([+-]|)(\\d*)n|)"+a+"*(?:([+-]|)"+a+"*(\\d+)|))"+a+"*\\)|)","i"),bool:new RegExp("^(?:"+Y+")$","i"),needsContext:new RegExp("^"+a+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+a+"*((?:-\\d)?\\d*)"+a+"*\\)|)(?=[^-]|$)","i")},re=/^(?:input|select|textarea|button)$/i,ie=/^h\d$/i,c=/^[^{]+\{\s*\[native \w/,oe=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,ae=/[+~]/,se=/'|\\/g,d=new RegExp("\\\\([\\da-f]{1,6}"+a+"?|("+a+")|.)","ig"),p=function(e,t,n){var r="0x"+t-65536;return r!=r||n?t:r<0?String.fromCharCode(65536+r):String.fromCharCode(r>>10|55296,1023&r|56320)},ue=function(){C()};try{D.apply(n=V.call(v.childNodes),v.childNodes),n[v.childNodes.length].nodeType}catch(F){D={apply:n.length?function(e,t){U.apply(e,V.call(t))}:function(e,t){for(var n=e.length,r=0;e[n++]=t[r++];);e.length=n-1}}}function H(e,t,n,r){var i,o,a,s,u,l,c,f,d=t&&t.ownerDocument,p=t?t.nodeType:9;if(n=n||[],"string"!=typeof e||!e||1!==p&&9!==p&&11!==p)return n;if(!r&&((t?t.ownerDocument||t:v)!==E&&C(t),t=t||E,N)){if(11!==p&&(l=oe.exec(e)))if(i=l[1]){if(9===p){if(!(a=t.getElementById(i)))return n;if(a.id===i)return n.push(a),n}else if(d&&(a=d.getElementById(i))&&y(t,a)&&a.id===i)return n.push(a),n}else{if(l[2])return D.apply(n,t.getElementsByTagName(e)),n;if((i=l[3])&&g.getElementsByClassName&&t.getElementsByClassName)return D.apply(n,t.getElementsByClassName(i)),n}if(g.qsa&&!A[e+" "]&&(!m||!m.test(e))){if(1!==p)d=t,f=e;else if("object"!==t.nodeName.toLowerCase()){for((s=t.getAttribute("id"))?s=s.replace(se,"\\$&"):t.setAttribute("id",s=k),o=(c=w(e)).length,u=ne.test(s)?"#"+s:"[id='"+s+"']";o--;)c[o]=u+" "+_(c[o]);f=c.join(","),d=ae.test(e)&&de(t.parentNode)||t}if(f)try{return D.apply(n,d.querySelectorAll(f)),n}catch(h){}finally{s===k&&t.removeAttribute("id")}}}return P(e.replace(L,"$1"),t,n,r)}function le(){var n=[];function r(e,t){return n.push(e+" ")>b.cacheLength&&delete r[n.shift()],r[e+" "]=t}return r}function q(e){return e[k]=!0,e}function h(e){var t=E.createElement("div");try{return!!e(t)}catch(F){return!1}finally{t.parentNode&&t.parentNode.removeChild(t)}}function ce(e,t){for(var n=e.split("|"),r=n.length;r--;)b.attrHandle[n[r]]=t}function fe(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&(~t.sourceIndex||1<<31)-(~e.sourceIndex||1<<31);if(r)return r;if(n)for(;n=n.nextSibling;)if(n===t)return-1;return e?1:-1}function x(a){return q(function(o){return o=+o,q(function(e,t){for(var n,r=a([],e.length,o),i=r.length;i--;)e[n=r[i]]&&(e[n]=!(t[n]=e[n]))})})}function de(e){return e&&"undefined"!=typeof e.getElementsByTagName&&e}for(e in g=H.support={},O=H.isXML=function(e){e=e&&(e.ownerDocument||e).documentElement;return!!e&&"HTML"!==e.nodeName},C=H.setDocument=function(e){var e=e?e.ownerDocument||e:v;return e!==E&&9===e.nodeType&&e.documentElement&&(t=(E=e).documentElement,N=!O(E),(e=E.defaultView)&&e.top!==e&&(e.addEventListener?e.addEventListener("unload",ue,!1):e.attachEvent&&e.attachEvent("onunload",ue)),g.attributes=h(function(e){return e.className="i",!e.getAttribute("className")}),g.getElementsByTagName=h(function(e){return e.appendChild(E.createComment("")),!e.getElementsByTagName("*").length}),g.getElementsByClassName=c.test(E.getElementsByClassName),g.getById=h(function(e){return t.appendChild(e).id=k,!E.getElementsByName||!E.getElementsByName(k).length}),g.getById?(b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&N)return(e=t.getElementById(e))?[e]:[]},b.filter.ID=function(e){var t=e.replace(d,p);return function(e){return e.getAttribute("id")===t}}):(delete b.find.ID,b.filter.ID=function(e){var t=e.replace(d,p);return function(e){e="undefined"!=typeof e.getAttributeNode&&e.getAttributeNode("id");return e&&e.value===t}}),b.find.TAG=g.getElementsByTagName?function(e,t){return"undefined"!=typeof t.getElementsByTagName?t.getElementsByTagName(e):g.qsa?t.querySelectorAll(e):void 0}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"!==e)return o;for(;n=o[i++];)1===n.nodeType&&r.push(n);return r},b.find.CLASS=g.getElementsByClassName&&function(e,t){if("undefined"!=typeof t.getElementsByClassName&&N)return t.getElementsByClassName(e)},r=[],m=[],(g.qsa=c.test(E.querySelectorAll))&&(h(function(e){t.appendChild(e).innerHTML="",e.querySelectorAll("[msallowcapture^='']").length&&m.push("[*^$]="+a+"*(?:''|\"\")"),e.querySelectorAll("[selected]").length||m.push("\\["+a+"*(?:value|"+Y+")"),e.querySelectorAll("[id~="+k+"-]").length||m.push("~="),e.querySelectorAll(":checked").length||m.push(":checked"),e.querySelectorAll("a#"+k+"+*").length||m.push(".#.+[+~]")}),h(function(e){var t=E.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),e.querySelectorAll("[name=d]").length&&m.push("name"+a+"*[*^$|!~]?="),e.querySelectorAll(":enabled").length||m.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),m.push(",.*:")})),(g.matchesSelector=c.test(i=t.matches||t.webkitMatchesSelector||t.mozMatchesSelector||t.oMatchesSelector||t.msMatchesSelector))&&h(function(e){g.disconnectedMatch=i.call(e,"div"),i.call(e,"[s!='']:x"),r.push("!=",G)}),m=m.length&&new RegExp(m.join("|")),r=r.length&&new RegExp(r.join("|")),e=c.test(t.compareDocumentPosition),y=e||c.test(t.contains)?function(e,t){var n=9===e.nodeType?e.documentElement:e,t=t&&t.parentNode;return e===t||!(!t||1!==t.nodeType||!(n.contains?n.contains(t):e.compareDocumentPosition&&16&e.compareDocumentPosition(t)))}:function(e,t){if(t)for(;t=t.parentNode;)if(t===e)return!0;return!1},$=e?function(e,t){var n;return e===t?(l=!0,0):(n=!e.compareDocumentPosition-!t.compareDocumentPosition)||(1&(n=(e.ownerDocument||e)===(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!g.sortDetached&&t.compareDocumentPosition(e)===n?e===E||e.ownerDocument===v&&y(v,e)?-1:t===E||t.ownerDocument===v&&y(v,t)?1:u?j(u,e)-j(u,t):0:4&n?-1:1)}:function(e,t){if(e===t)return l=!0,0;var n,r=0,i=e.parentNode,o=t.parentNode,a=[e],s=[t];if(!i||!o)return e===E?-1:t===E?1:i?-1:o?1:u?j(u,e)-j(u,t):0;if(i===o)return fe(e,t);for(n=e;n=n.parentNode;)a.unshift(n);for(n=t;n=n.parentNode;)s.unshift(n);for(;a[r]===s[r];)r++;return r?fe(a[r],s[r]):a[r]===v?-1:s[r]===v?1:0}),E},H.matches=function(e,t){return H(e,null,null,t)},H.matchesSelector=function(e,t){if((e.ownerDocument||e)!==E&&C(e),t=t.replace(ee,"='$1']"),g.matchesSelector&&N&&!A[t+" "]&&(!r||!r.test(t))&&(!m||!m.test(t)))try{var n=i.call(e,t);if(n||g.disconnectedMatch||e.document&&11!==e.document.nodeType)return n}catch(F){}return 0":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(d,p),e[3]=(e[3]||e[4]||e[5]||"").replace(d,p),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||H.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&H.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return f.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&te.test(n)&&(t=(t=w(n,!0))&&n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(d,p).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=W[e+" "];return t||(t=new RegExp("(^|"+a+")"+e+"("+a+"|$)"))&&W(e,function(e){return t.test("string"==typeof e.className&&e.className||"undefined"!=typeof e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(t,n,r){return function(e){e=H.attr(e,t);return null==e?"!="===n:!n||(e+="","="===n?e===r:"!="===n?e!==r:"^="===n?r&&0===e.indexOf(r):"*="===n?r&&-1(?:<\/\1>|)$/,G=/^.[^:#\[\.,]*$/;function K(e,n,r){if(C.isFunction(n))return C.grep(e,function(e,t){return!!n.call(e,t,e)!==r});if(n.nodeType)return C.grep(e,function(e){return e===n!==r});if("string"==typeof n){if(G.test(n))return C.filter(n,e,r);n=C.filter(n,e)}return C.grep(e,function(e){return-1)[^>]*|#([\w-]*))$/,ee=((C.fn.init=function(e,t,n){if(e){if(n=n||Q,"string"!=typeof e)return e.nodeType?(this.context=this[0]=e,this.length=1,this):C.isFunction(e)?"undefined"!=typeof n.ready?n.ready(e):e(C):(e.selector!==undefined&&(this.selector=e.selector,this.context=e.context),C.makeArray(e,this));if(!(r="<"===e.charAt(0)&&">"===e.charAt(e.length-1)&&3<=e.length?[null,e,null]:Z.exec(e))||!r[1]&&t)return(!t||t.jquery?t||n:this.constructor(t)).find(e);if(r[1]){if(t=t instanceof C?t[0]:t,C.merge(this,C.parseHTML(r[1],t&&t.nodeType?t.ownerDocument||t:g,!0)),J.test(r[1])&&C.isPlainObject(t))for(var r in t)C.isFunction(this[r])?this[r](t[r]):this.attr(r,t[r])}else{if((n=g.getElementById(r[2]))&&n.parentNode){if(n.id!==r[2])return Q.find(e);this.length=1,this[0]=n}this.context=g,this.selector=e}}return this}).prototype=C.fn,Q=C(g),/^(?:parents|prev(?:Until|All))/),te={children:!0,contents:!0,next:!0,prev:!0};function ne(e,t){for(;(e=e[t])&&1!==e.nodeType;);return e}C.fn.extend({has:function(e){var t,n=C(e,this),r=n.length;return this.filter(function(){for(t=0;t
    a",y.leadingWhitespace=3===S.firstChild.nodeType,y.tbody=!S.getElementsByTagName("tbody").length,y.htmlSerialize=!!S.getElementsByTagName("link").length,y.html5Clone="<:nav>"!==g.createElement("nav").cloneNode(!0).outerHTML,q.type="checkbox",q.checked=!0,k.appendChild(q),y.appendChecked=q.checked,S.innerHTML="",y.noCloneChecked=!!S.cloneNode(!0).lastChild.defaultValue,k.appendChild(S),(q=g.createElement("input")).setAttribute("type","radio"),q.setAttribute("checked","checked"),q.setAttribute("name","t"),S.appendChild(q),y.checkClone=S.cloneNode(!0).cloneNode(!0).lastChild.checked,y.noCloneEvent=!!S.addEventListener,S[C.expando]=1,y.attributes=!S.getAttribute(C.expando);var x={option:[1,""],legend:[1,"
    ","
    "],area:[1,"",""],param:[1,"",""],thead:[1,"","
    "],tr:[2,"","
    "],col:[2,"","
    "],td:[3,"","
    "],_default:y.htmlSerialize?[0,"",""]:[1,"X
    ","
    "]};function b(e,t){var n,r,i=0,o="undefined"!=typeof e.getElementsByTagName?e.getElementsByTagName(t||"*"):"undefined"!=typeof e.querySelectorAll?e.querySelectorAll(t||"*"):undefined;if(!o)for(o=[],n=e.childNodes||e;null!=(r=n[i]);i++)!t||C.nodeName(r,t)?o.push(r):C.merge(o,b(r,t));return t===undefined||t&&C.nodeName(e,t)?C.merge([e],o):o}function we(e,t){for(var n,r=0;null!=(n=e[r]);r++)C._data(n,"globalEval",!t||C._data(t[r],"globalEval"))}x.optgroup=x.option,x.tbody=x.tfoot=x.colgroup=x.caption=x.thead,x.th=x.td;var Te=/<|&#?\w+;/,Ce=/"!==f[1]||Ce.test(a)?0:u:u.firstChild)&&a.childNodes.length;o--;)C.nodeName(c=a.childNodes[o],"tbody")&&!c.childNodes.length&&a.removeChild(c);for(C.merge(h,u.childNodes),u.textContent="";u.firstChild;)u.removeChild(u.firstChild);u=p.lastChild}else h.push(t.createTextNode(a));for(u&&p.removeChild(u),y.appendChecked||C.grep(b(h,"input"),Ee),g=0;a=h[g++];)if(r&&-1]","i"),Pe=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:-]+)[^>]*)\/>/gi,Be=/\s*$/g,ze=be(g).appendChild(g.createElement("div"));function Xe(e,t){return C.nodeName(e,"table")&&C.nodeName(11!==t.nodeType?t:t.firstChild,"tr")?e.getElementsByTagName("tbody")[0]||e.appendChild(e.ownerDocument.createElement("tbody")):e}function Ue(e){return e.type=(null!==C.find.attr(e,"type"))+"/"+e.type,e}function Ve(e){var t=Ie.exec(e.type);return t?e.type=t[1]:e.removeAttribute("type"),e}function Ye(e,t){if(1===t.nodeType&&C.hasData(e)){var n,r,i,e=C._data(e),o=C._data(t,e),a=e.events;if(a)for(n in delete o.handle,o.events={},a)for(r=0,i=a[n].length;r")},clone:function(e,t,n){var r,i,o,a,s,u=C.contains(e.ownerDocument,e);if(y.html5Clone||C.isXMLDoc(e)||!Re.test("<"+e.nodeName+">")?o=e.cloneNode(!0):(ze.innerHTML=e.outerHTML,ze.removeChild(o=ze.firstChild)),!(y.noCloneEvent&&y.noCloneChecked||1!==e.nodeType&&11!==e.nodeType||C.isXMLDoc(e)))for(r=b(o),s=b(e),a=0;null!=(i=s[a]);++a)if(r[a]){f=c=l=p=d=void 0;var l,c,f,d=i,p=r[a];if(1===p.nodeType){if(l=p.nodeName.toLowerCase(),!y.noCloneEvent&&p[C.expando]){for(c in(f=C._data(p)).events)C.removeEvent(p,c,f.handle);p.removeAttribute(C.expando)}"script"===l&&p.text!==d.text?(Ue(p).text=d.text,Ve(p)):"object"===l?(p.parentNode&&(p.outerHTML=d.outerHTML),y.html5Clone&&d.innerHTML&&!C.trim(p.innerHTML)&&(p.innerHTML=d.innerHTML)):"input"===l&&ge.test(d.type)?(p.defaultChecked=p.checked=d.checked,p.value!==d.value&&(p.value=d.value)):"option"===l?p.defaultSelected=p.selected=d.defaultSelected:"input"!==l&&"textarea"!==l||(p.defaultValue=d.defaultValue)}}if(t)if(n)for(s=s||b(e),r=r||b(o),a=0;null!=(i=s[a]);a++)Ye(i,r[a]);else Ye(e,o);return 0<(r=b(o,"script")).length&&we(r,!u&&b(e,"script")),r=s=i=null,o},cleanData:function(e,t){for(var n,r,i,o,a=0,s=C.expando,u=C.cache,l=y.attributes,c=C.event.special;null!=(n=e[a]);a++)if((t||v(n))&&(o=(i=n[s])&&u[i])){if(o.events)for(r in o.events)c[r]?C.event.remove(n,r):C.removeEvent(n,r,o.handle);u[i]&&(delete u[i],l||"undefined"==typeof n.removeAttribute?n[s]=undefined:n.removeAttribute(s),f.push(i))}}}),C.fn.extend({domManip:w,detach:function(e){return Je(this,e,!0)},remove:function(e){return Je(this,e)},text:function(e){return d(this,function(e){return e===undefined?C.text(this):this.empty().append((this[0]&&this[0].ownerDocument||g).createTextNode(e))},null,e,arguments.length)},append:function(){return w(this,arguments,function(e){1!==this.nodeType&&11!==this.nodeType&&9!==this.nodeType||Xe(this,e).appendChild(e)})},prepend:function(){return w(this,arguments,function(e){var t;1!==this.nodeType&&11!==this.nodeType&&9!==this.nodeType||(t=Xe(this,e)).insertBefore(e,t.firstChild)})},before:function(){return w(this,arguments,function(e){this.parentNode&&this.parentNode.insertBefore(e,this)})},after:function(){return w(this,arguments,function(e){this.parentNode&&this.parentNode.insertBefore(e,this.nextSibling)})},empty:function(){for(var e,t=0;null!=(e=this[t]);t++){for(1===e.nodeType&&C.cleanData(b(e,!1));e.firstChild;)e.removeChild(e.firstChild);e.options&&C.nodeName(e,"select")&&(e.options.length=0)}return this},clone:function(e,t){return e=null!=e&&e,t=null==t?e:t,this.map(function(){return C.clone(this,e,t)})},html:function(e){return d(this,function(e){var t=this[0]||{},n=0,r=this.length;if(e===undefined)return 1===t.nodeType?t.innerHTML.replace(Oe,""):undefined;if("string"==typeof e&&!Be.test(e)&&(y.htmlSerialize||!Re.test(e))&&(y.leadingWhitespace||!ve.test(e))&&!x[(me.exec(e)||["",""])[1].toLowerCase()]){e=C.htmlPrefilter(e);try{for(;n")).appendTo(t.documentElement))[0].contentWindow||Ge[0].contentDocument).document).write(),t.close(),n=Qe(e,t),Ge.detach()),Ke[e]=n),n}var n,et,tt,nt,rt,it,ot,a,at=/^margin/,st=new RegExp("^("+e+")(?!px)[a-z%]+$","i"),ut=function(e,t,n,r){var i,o={};for(i in t)o[i]=e.style[i],e.style[i]=t[i];for(i in r=n.apply(e,r||[]),t)e.style[i]=o[i];return r},lt=g.documentElement;function t(){var e,t=g.documentElement;t.appendChild(ot),a.style.cssText="-webkit-box-sizing:border-box;box-sizing:border-box;position:relative;display:block;margin:auto;border:1px;padding:1px;top:1%;width:50%",n=tt=it=!1,et=rt=!0,T.getComputedStyle&&(e=T.getComputedStyle(a),n="1%"!==(e||{}).top,it="2px"===(e||{}).marginLeft,tt="4px"===(e||{width:"4px"}).width,a.style.marginRight="50%",et="4px"===(e||{marginRight:"4px"}).marginRight,(e=a.appendChild(g.createElement("div"))).style.cssText=a.style.cssText="-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;display:block;margin:0;border:0;padding:0",e.style.marginRight=e.style.width="0",a.style.width="1px",rt=!parseFloat((T.getComputedStyle(e)||{}).marginRight),a.removeChild(e)),a.style.display="none",(nt=0===a.getClientRects().length)&&(a.style.display="",a.innerHTML="
    t
    ",a.childNodes[0].style.borderCollapse="separate",(e=a.getElementsByTagName("td"))[0].style.cssText="margin:0;border:0;padding:0;display:none",nt=0===e[0].offsetHeight)&&(e[0].style.display="",e[1].style.display="none",nt=0===e[0].offsetHeight),t.removeChild(ot)}ot=g.createElement("div"),(a=g.createElement("div")).style&&(a.style.cssText="float:left;opacity:.5",y.opacity="0.5"===a.style.opacity,y.cssFloat=!!a.style.cssFloat,a.style.backgroundClip="content-box",a.cloneNode(!0).style.backgroundClip="",y.clearCloneStyle="content-box"===a.style.backgroundClip,(ot=g.createElement("div")).style.cssText="border:0;width:8px;height:0;top:0;left:-9999px;padding:0;margin-top:1px;position:absolute",a.innerHTML="",ot.appendChild(a),y.boxSizing=""===a.style.boxSizing||""===a.style.MozBoxSizing||""===a.style.WebkitBoxSizing,C.extend(y,{reliableHiddenOffsets:function(){return null==n&&t(),nt},boxSizingReliable:function(){return null==n&&t(),tt},pixelMarginRight:function(){return null==n&&t(),et},pixelPosition:function(){return null==n&&t(),n},reliableMarginRight:function(){return null==n&&t(),rt},reliableMarginLeft:function(){return null==n&&t(),it}}));var l,p,ct=/^(top|right|bottom|left)$/;function ft(e,t){return{get:function(){if(!e())return(this.get=t).apply(this,arguments);delete this.get}}}T.getComputedStyle?(l=function(e){var t=e.ownerDocument.defaultView;return(t=t&&t.opener?t:T).getComputedStyle(e)},p=function(e,t,n){var r,i,o=e.style;return""!==(i=(n=n||l(e))?n.getPropertyValue(t)||n[t]:undefined)&&i!==undefined||C.contains(e.ownerDocument,e)||(i=C.style(e,t)),n&&!y.pixelMarginRight()&&st.test(i)&&at.test(t)&&(e=o.width,t=o.minWidth,r=o.maxWidth,o.minWidth=o.maxWidth=o.width=i,i=n.width,o.width=e,o.minWidth=t,o.maxWidth=r),i===undefined?i:i+""}):lt.currentStyle&&(l=function(e){return e.currentStyle},p=function(e,t,n){var r,i,o,a=e.style;return null==(n=(n=n||l(e))?n[t]:undefined)&&a&&a[t]&&(n=a[t]),st.test(n)&&!ct.test(t)&&(r=a.left,(o=(i=e.runtimeStyle)&&i.left)&&(i.left=e.currentStyle.left),a.left="fontSize"===t?"1em":n,n=a.pixelLeft+"px",a.left=r,o)&&(i.left=o),n===undefined?n:n+""||"auto"});var dt=/alpha\([^)]*\)/i,pt=/opacity\s*=\s*([^)]*)/i,ht=/^(none|table(?!-c[ea]).+)/,gt=new RegExp("^("+e+")(.*)$","i"),mt={position:"absolute",visibility:"hidden",display:"block"},yt={letterSpacing:"0",fontWeight:"400"},vt=["Webkit","O","Moz","ms"],xt=g.createElement("div").style;function bt(e){if(e in xt)return e;for(var t=e.charAt(0).toUpperCase()+e.slice(1),n=vt.length;n--;)if((e=vt[n]+t)in xt)return e}function wt(e,t){for(var n,r,i,o=[],a=0,s=e.length;a
    a",F=q.getElementsByTagName("a")[0],k.setAttribute("type","checkbox"),q.appendChild(k),(F=q.getElementsByTagName("a")[0]).style.cssText="top:1px",y.getSetAttribute="t"!==q.className,y.style=/top/.test(F.getAttribute("style")),y.hrefNormalized="/a"===F.getAttribute("href"),y.checkOn=!!k.value,y.optSelected=e.selected,y.enctype=!!g.createElement("form").enctype,S.disabled=!0,y.optDisabled=!e.disabled,(k=g.createElement("input")).setAttribute("value",""),y.input=""===k.getAttribute("value"),k.value="t",k.setAttribute("type","radio"),y.radioValue="t"===k.value;var Lt=/\r/g,Ht=/[\x20\t\r\n\f]+/g;C.fn.extend({val:function(t){var n,e,r,i=this[0];return arguments.length?(r=C.isFunction(t),this.each(function(e){1!==this.nodeType||(null==(e=r?t.call(this,e,C(this).val()):t)?e="":"number"==typeof e?e+="":C.isArray(e)&&(e=C.map(e,function(e){return null==e?"":e+""})),(n=C.valHooks[this.type]||C.valHooks[this.nodeName.toLowerCase()])&&"set"in n&&n.set(this,e,"value")!==undefined)||(this.value=e)})):i?(n=C.valHooks[i.type]||C.valHooks[i.nodeName.toLowerCase()])&&"get"in n&&(e=n.get(i,"value"))!==undefined?e:"string"==typeof(e=i.value)?e.replace(Lt,""):null==e?"":e:void 0}}),C.extend({valHooks:{option:{get:function(e){var t=C.find.attr(e,"value");return null!=t?t:C.trim(C.text(e)).replace(Ht," ")}},select:{get:function(e){for(var t,n=e.options,r=e.selectedIndex,i="select-one"===e.type||r<0,o=i?null:[],a=i?r+1:n.length,s=r<0?a:i?r:0;s").append(C.parseHTML(e)).find(r):e)}).always(n&&function(e,t){a.each(function(){n.apply(this,o||[e.responseText,t,e])})}),this)},C.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(e,t){C.fn[t]=function(e){return this.on(t,e)}}),C.expr.filters.animated=function(t){return C.grep(C.timers,function(e){return t===e.elem}).length},C.offset={setOffset:function(e,t,n){var r,i,o,a,s=C.css(e,"position"),u=C(e),l={};"static"===s&&(e.style.position="relative"),o=u.offset(),r=C.css(e,"top"),a=C.css(e,"left"),s=("absolute"===s||"fixed"===s)&&-1'+(s?a.title[0]:a.title)+"":"";return a.zIndex=o,t([a.shade?'
    ':"",'
    '+(e&&2!=a.type?"":s)+"'+(n=["layui-icon-tips","layui-icon-success","layui-icon-error","layui-icon-question","layui-icon-lock","layui-icon-face-cry","layui-icon-face-smile"],o="layui-anim layui-anim-rotate layui-anim-loop",0==a.type&&-1!==a.icon?'':3==a.type?(i=["layui-icon-loading","layui-icon-loading-1"],2==a.icon?'
    ':''):"")+((1!=a.type||!e)&&a.content||"")+'
    '+(n=[],l&&(n.push(''),n.push('')),a.closeBtn&&n.push(''),n.join(""))+"
    "+(a.btn?function(){var e="";"string"==typeof a.btn&&(a.btn=[a.btn]);for(var t,i=0,n=a.btn.length;i'+a.btn[i]+"";return'
    '+e+"
    "}():"")+(a.resize?'':"")+""],s,m('
    ')),this},t.pt.creat=function(){var e,t,i,n,a,o=this,s=o.config,l=o.index,r="object"==typeof(f=s.content),c=m("body");if(s.id&&m("."+y[0]).find("#"+s.id)[0])e=m("#"+s.id).closest("."+y[0]),t=e.attr("times"),i=e.data("config"),n=m("#"+y.SHADE+t),"min"===(e.data("maxminStatus")||{})?h.restore(t):i.hideOnClose&&(n.show(),e.show());else{switch(s.removeFocus&&document.activeElement.blur(),"string"==typeof s.area&&(s.area="auto"===s.area?["",""]:[s.area,""]),s.shift&&(s.anim=s.shift),6==h.ie&&(s.fixed=!1),s.type){case 0:s.btn="btn"in s?s.btn:u.btn[0],h.closeAll("dialog");break;case 2:var f=s.content=r?s.content:[s.content||"","auto"];s.content='';break;case 3:delete s.title,delete s.closeBtn,-1===s.icon&&s.icon,h.closeAll("loading");break;case 4:r||(s.content=[s.content,"body"]),s.follow=s.content[1],s.content=s.content[0]+'',delete s.title,s.tips="object"==typeof s.tips?s.tips:[s.tips,!0],s.tipsMore||h.closeAll("tips")}o.vessel(r,function(e,t,i){c.append(e[0]),r?2==s.type||4==s.type?m("body").append(e[1]):f.parents("."+y[0])[0]||(f.data("display",f.css("display")).show().addClass("layui-layer-wrap").wrap(e[1]),m("#"+y[0]+l).find("."+y[5]).before(t)):c.append(e[1]),m("#"+y.MOVE)[0]||c.append(u.moveElem=i),o.layero=m("#"+y[0]+l),o.shadeo=m("#"+y.SHADE+l),s.scrollbar||u.setScrollbar(l)}).auto(l),o.shadeo.css({"background-color":s.shade[1]||"#000",opacity:s.shade[0]||s.shade}),2==s.type&&6==h.ie&&o.layero.find("iframe").attr("src",f[0]),4==s.type?o.tips():(o.offset(),parseInt(u.getStyle(document.getElementById(y.MOVE),"z-index"))||(o.layero.css("visibility","hidden"),h.ready(function(){o.offset(),o.layero.css("visibility","visible")}))),!s.fixed||u.events.resize[o.index]||(u.events.resize[o.index]=function(){o.resize()},d.on("resize",u.events.resize[o.index])),s.time<=0||setTimeout(function(){h.close(o.index)},s.time),o.move().callback(),y.anim[s.anim]&&(a="layer-anim "+y.anim[s.anim],o.layero.addClass(a).one("webkitAnimationEnd mozAnimationEnd MSAnimationEnd oanimationend animationend",function(){m(this).removeClass(a)})),o.layero.data("config",s)}},t.pt.resize=function(){var e=this,t=e.config;e.offset(),(/^\d+%$/.test(t.area[0])||/^\d+%$/.test(t.area[1]))&&e.auto(e.index),4==t.type&&e.tips()},t.pt.auto=function(e){var t=this.config,i=m("#"+y[0]+e),n=(""===t.area[0]&&0t.maxWidth)&&i.width(t.maxWidth),[i.innerWidth(),i.innerHeight()]),a=i.find(y[1]).outerHeight()||0,o=i.find("."+y[6]).outerHeight()||0,e=function(e){(e=i.find(e)).height(n[1]-a-o-2*(0|parseFloat(e.css("padding-top"))))};return 2===t.type?e("iframe"):""===t.area[1]?0t.maxHeight?(n[1]=t.maxHeight,e("."+y[5])):t.fixed&&n[1]>=d.height()&&(n[1]=d.height(),e("."+y[5])):e("."+y[5]),this},t.pt.offset=function(){var e=this,t=e.config,i=e.layero,n=[i.outerWidth(),i.outerHeight()],a="object"==typeof t.offset;e.offsetTop=(d.height()-n[1])/2,e.offsetLeft=(d.width()-n[0])/2,a?(e.offsetTop=t.offset[0],e.offsetLeft=t.offset[1]||e.offsetLeft):"auto"!==t.offset&&("t"===t.offset?e.offsetTop=0:"r"===t.offset?e.offsetLeft=d.width()-n[0]:"b"===t.offset?e.offsetTop=d.height()-n[1]:"l"===t.offset?e.offsetLeft=0:"lt"===t.offset?(e.offsetTop=0,e.offsetLeft=0):"lb"===t.offset?(e.offsetTop=d.height()-n[1],e.offsetLeft=0):"rt"===t.offset?(e.offsetTop=0,e.offsetLeft=d.width()-n[0]):"rb"===t.offset?(e.offsetTop=d.height()-n[1],e.offsetLeft=d.width()-n[0]):e.offsetTop=t.offset),t.fixed||(e.offsetTop=/%$/.test(e.offsetTop)?d.height()*parseFloat(e.offsetTop)/100:parseFloat(e.offsetTop),e.offsetLeft=/%$/.test(e.offsetLeft)?d.width()*parseFloat(e.offsetLeft)/100:parseFloat(e.offsetLeft),e.offsetTop+=d.scrollTop(),e.offsetLeft+=d.scrollLeft()),"min"===i.data("maxminStatus")&&(e.offsetTop=d.height()-(i.find(y[1]).outerHeight()||0),e.offsetLeft=i.css("left")),i.css({top:e.offsetTop,left:e.offsetLeft})},t.pt.tips=function(){var e=this.config,t=this.layero,i=[t.outerWidth(),t.outerHeight()],n=m(e.follow),a={width:(n=n[0]?n:m("body")).outerWidth(),height:n.outerHeight(),top:n.offset().top,left:n.offset().left},o=t.find(".layui-layer-TipsG"),n=e.tips[0];e.tips[1]||o.remove(),a.autoLeft=function(){0d.width()&&(o=d.width()-180-(u.minStackArr.edgeIndex=u.minStackArr.edgeIndex||0,u.minStackArr.edgeIndex+=3))<0&&(o=0),t.minStack&&(l.left=o,l.top=d.height()-n,a||u.minStackIndex++,r.attr("minLeft",o)),r.attr("position",s),h.style(e,l,!0),i.hide(),"page"===r.attr("type")&&r.find(y[4]).hide(),u.restScrollbar(e),c.hide())},h.restore=function(e){var t=m("#"+y[0]+e),i=m("#"+y.SHADE+e),n=t.attr("area").split(","),a=t.attr("type"),o=t.data("config")||{};t.removeData("maxminStatus"),h.style(e,{width:n[0],height:n[1],top:parseFloat(n[2]),left:parseFloat(n[3]),position:t.attr("position"),overflow:"visible"},!0),t.find(".layui-layer-max").removeClass("layui-layer-maxmin"),t.find(".layui-layer-min").show(),"page"===a&&t.find(y[4]).show(),o.scrollbar?u.restScrollbar(e):u.setScrollbar(e),i.show()},h.full=function(t){var i=m("#"+y[0]+t),e=i.data("maxminStatus");"max"!==e&&("min"===e&&h.restore(t),i.data("maxminStatus","max"),u.record(i),y.html.attr("layer-full")||u.setScrollbar(t),setTimeout(function(){var e="fixed"===i.css("position");h.style(t,{top:e?0:d.scrollTop(),left:e?0:d.scrollLeft(),width:"100%",height:"100%"},!0),i.find(".layui-layer-min").hide()},100))},h.title=function(e,t){m("#"+y[0]+(t||h.index)).find(y[1]).html(e)},h.close=function(o,s){var l,e,r=(t=m("."+y[0]).children("#"+o).closest("."+y[0]))[0]?(o=t.attr("times"),t):m("#"+y[0]+o),c=r.attr("type"),t=r.data("config")||{},f=t.id&&t.hideOnClose;r[0]&&(l={slideDown:"layer-anim-slide-down-out",slideLeft:"layer-anim-slide-left-out",slideUp:"layer-anim-slide-up-out",slideRight:"layer-anim-slide-right-out"}[t.anim]||"layer-anim-close",e=function(){var e="layui-layer-wrap";if(f)return r.removeClass("layer-anim "+l),r.hide();if(c===u.type[1]&&"object"===r.attr("conType")){r.children(":not(."+y[5]+")").remove();for(var t=r.find("."+e),i=0;i<2;i++)t.unwrap();t.css("display",t.data("display")).removeClass(e)}else{if(c===u.type[2])try{var n=m("#"+y[4]+o)[0];n.contentWindow.document.write(""),n.contentWindow.close(),r.find("."+y[5])[0].removeChild(n)}catch(a){}r[0].innerHTML="",r.remove()}"function"==typeof u.end[o]&&u.end[o](),delete u.end[o],"function"==typeof s&&s(),u.events.resize[o]&&(d.off("resize",u.events.resize[o]),delete u.events.resize[o])},m("#"+y.SHADE+o)[f?"hide":"remove"](),t.isOutAnim&&r.addClass("layer-anim "+l),6==h.ie&&u.reselect(),u.restScrollbar(o),"string"==typeof r.attr("minLeft")&&(u.minStackIndex--,u.minStackArr.push(r.attr("minLeft"))),h.ie&&h.ie<10||!t.isOutAnim?e():setTimeout(function(){e()},200))},h.closeAll=function(n,a){"function"==typeof n&&(a=n,n=null);var o=m("."+y[0]);m.each(o,function(e){var t=m(this),i=n?t.attr("type")===n:1;i&&h.close(t.attr("times"),e===o.length-1?a:null)}),0===o.length&&"function"==typeof a&&a()},h.closeLast=function(e){h.close(m(".layui-layer-"+(e=e||"page")+":last").attr("times"))},h.cache||{}),g=function(e){return i.skin?" "+i.skin+" "+i.skin+"-"+e:""};h.prompt=function(i,n){var e="",t="";"function"==typeof(i=i||{})&&(n=i),i.area&&(e='style="width: '+(o=i.area)[0]+"; height: "+o[1]+';"',delete i.area),i.placeholder&&(t=' placeholder="'+i.placeholder+'"');var a,o=2==i.formType?'":'",s=i.success;return delete i.success,h.open(m.extend({type:1,btn:["确定","取消"],content:o,skin:"layui-layer-prompt"+g("prompt"),maxWidth:d.width(),success:function(e){(a=e.find(".layui-layer-input")).val(i.value||"").focus(),"function"==typeof s&&s(e)},resize:!1,yes:function(e){var t=a.val();t.length>(i.maxlength||500)?h.tips("最多输入"+(i.maxlength||500)+"个字数",a,{tips:1}):n&&n(t,e,a)}},i))},h.tab=function(n){var a=(n=n||{}).tab||{},o="layui-this",s=n.success;return delete n.success,h.open(m.extend({type:1,skin:"layui-layer-tab"+g("tab"),resize:!1,title:function(){var e=a.length,t=1,i="";if(0'+a[0].title+"";t"+a[t].title+"";return i}(),content:'
      '+function(){var e=a.length,t=1,i="";if(0'+(a[0].content||"no content")+"";t'+(a[t].content||"no content")+"";return i}()+"
    ",success:function(e){var t=e.find(".layui-layer-title").children(),i=e.find(".layui-layer-tabmain").children();t.on("mousedown",function(e){e.stopPropagation?e.stopPropagation():e.cancelBubble=!0;var e=m(this),t=e.index();e.addClass(o).siblings().removeClass(o),i.eq(t).show().siblings().hide(),"function"==typeof n.change&&n.change(t)}),"function"==typeof s&&s(e)}},n))},h.photos=function(n,e,a){var o={};if((n=m.extend(!0,{toolbar:!0,footer:!0},n)).photos){var t=!("string"==typeof n.photos||n.photos instanceof m),i=t?n.photos:{},s=i.data||[],l=i.start||0,r=n.success;if(o.imgIndex=1+(0|l),n.img=n.img||"img",delete n.success,t){if(0===s.length)return h.msg("没有图片")}else{var c=m(n.photos),f=function(){s=[],c.find(n.img).each(function(e){var t=m(this);t.attr("layer-index",e),s.push({alt:t.attr("alt"),pid:t.attr("layer-pid"),src:t.attr("lay-src")||t.attr("layer-src")||t.attr("src"),thumb:t.attr("src")})})};if(f(),0===s.length)return;if(e||c.on("click",n.img,function(){f();var e=m(this).attr("layer-index");h.photos(m.extend(n,{photos:{start:e,data:s,tab:n.tab},full:n.full}),!0)}),!e)return}o.imgprev=function(e){o.imgIndex--,o.imgIndex<1&&(o.imgIndex=s.length),o.tabimg(e)},o.imgnext=function(e,t){o.imgIndex++,o.imgIndex>s.length&&(o.imgIndex=1,t)||o.tabimg(e)},o.keyup=function(e){var t;o.end||(t=e.keyCode,e.preventDefault(),37===t?o.imgprev(!0):39===t?o.imgnext(!0):27===t&&h.close(o.index))},o.tabimg=function(e){if(!(s.length<=1))return i.start=o.imgIndex-1,h.close(o.index),h.photos(n,!0,e)},o.isNumber=function(e){return"number"==typeof e&&!isNaN(e)},o.image={},o.getTransform=function(e){var t=[],i=e.rotate,n=e.scaleX,e=e.scale;return o.isNumber(i)&&0!==i&&t.push("rotate("+i+"deg)"),o.isNumber(n)&&1!==n&&t.push("scaleX("+n+")"),o.isNumber(e)&&t.push("scale("+e+")"),t.length?t.join(" "):"none"},o.event=function(e,i,n){o.main.find(".layui-layer-photos-prev").on("click",function(e){e.preventDefault(),o.imgprev(!0)}),o.main.find(".layui-layer-photos-next").on("click",function(e){e.preventDefault(),o.imgnext(!0)}),m(document).on("keyup",o.keyup),e.off("click").on("click","*[toolbar-event]",function(){var e=m(this);switch(e.attr("toolbar-event")){case"rotate":o.image.rotate=((o.image.rotate||0)+Number(e.attr("data-option")))%360,o.imgElem.css({transform:o.getTransform(o.image)});break;case"scalex":o.image.scaleX=-1===o.image.scaleX?1:-1,o.imgElem.css({transform:o.getTransform(o.image)});break;case"zoom":var t=Number(e.attr("data-option"));o.image.scale=(o.image.scale||1)+t,t<0&&o.image.scale<0-t&&(o.image.scale=0-t),o.imgElem.css({transform:o.getTransform(o.image)});break;case"reset":o.image.scaleX=1,o.image.scale=1,o.image.rotate=0,o.imgElem.css({transform:"none"});break;case"close":h.close(i)}n.offset(),n.auto(i)}),o.main.on("mousewheel DOMMouseScroll",function(e){var e=e.originalEvent.wheelDelta||-e.originalEvent.detail,t=o.main.find('[toolbar-event="zoom"]');(0'+i+''+(t=['
    '],1','','',"
    "].join("")),n.toolbar&&t.push(['
    ','','','','','','',"
    "].join("")),n.footer&&t.push(['"].join("")),t.push(""),t.join(""))+"",success:function(e,t,i){o.main=e.find(".layer-layer-photos-main"),o.footer=e.find(".layui-layer-photos-footer"),o.imgElem=o.main.children("img"),o.event(e,t,i),n.tab&&n.tab(s[l],e),"function"==typeof r&&r(e)},end:function(){o.end=!0,m(document).off("keyup",o.keyup)}},n))},u=function(){h.close(o.loadi),h.msg("当前图片地址异常
    是否继续查看下一张?",{time:3e4,btn:["下一张","不看了"],yes:function(){1").addClass(r));layui.each(i.bars,function(t,e){var n=s('
  • ');n.addClass(e.icon).attr({"lay-type":e.type,style:e.style||(i.bgcolor?"background-color: "+i.bgcolor:"")}).html(e.content),n.on("click",function(){var t=s(this).attr("lay-type");"top"===t&&("body"===i.target?s("html,body"):u).animate({scrollTop:0},i.duration),"function"==typeof i.click&&i.click.call(this,t)}),"object"===layui.type(i.on)&&layui.each(i.on,function(t,e){n.on(t,function(){var t=s(this).attr("lay-type");"function"==typeof e&&e.call(this,t)})}),"top"===e.type&&(n.addClass("layui-fixbar-top"),o=n),l.append(n)}),c.find("."+r).remove(),"object"==typeof i.css&&l.css(i.css),c.append(l),o&&(e=function e(){return u.scrollTop()>=i.margin?t||(o.show(),t=1):t&&(o.hide(),t=0),e}()),u.on("scroll",function(){e&&(clearTimeout(n),n=setTimeout(function(){e()},100))})},countdown:function(i){i=s.extend(!0,{date:new Date,now:new Date},i);var o=arguments,r=(1]|&(?=#[a-zA-Z0-9]+)/g.test(t+="")?t.replace(/&(?!#?[a-zA-Z0-9]+;)/g,"&").replace(//g,">").replace(/'/g,"'").replace(/"/g,"""):t},unescape:function(t){return t!==undefined&&null!==t||(t=""),(t+="").replace(/\&/g,"&").replace(/\</g,"<").replace(/\>/g,">").replace(/\'/g,"'").replace(/\"/g,'"')},openWin:function(t){var e=(t=t||{}).window||window.open(t.url||"",t.target,t.specs);t.url||(e.document.open("text/html","replace"),e.document.write(t.content||""),e.document.close())},toVisibleArea:function(t){var e,n,i,o,r,a,c,u;(t=s.extend({margin:160,duration:200,type:"y"},t)).scrollElem[0]&&t.thisElem[0]&&(e=t.scrollElem,c=t.thisElem,i=(r="y"===t.type)?"top":"left",o=e[n=r?"scrollTop":"scrollLeft"](),r=e[r?"height":"width"](),a=e.offset()[i],u={},(c=c.offset()[i]-a)>r-t.margin||c."+v,E=function(e){var i=this;i.index=++f.index,i.config=c.extend({},i.config,f.config,e),i.init()};E.prototype.config={trigger:"click",content:"",className:"",style:"",show:!1,isAllowSpread:!0,isSpreadItem:!0,data:[],delay:300,shade:0,accordion:!1},E.prototype.reload=function(e,i){var t=this;t.config=c.extend({},t.config,e),t.init(!0,i)},E.prototype.init=function(e,i){var t,n=this,a=n.config,l=c(a.elem);return 1');return 0No data
  • '),e},u=function(r,e){return layui.each(e,function(e,i){var t,n=i[s.children]&&0",(t="href"in i?''+l+"":l,n?'
    '+t+("parent"===o?'':"group"===o&&d.isAllowSpread?'':"")+"
    ":'
    '+t+"
    "),""].join(""))).data("item",i),n&&(a=c('
    '),t=c("
      "),"parent"===o?(a.append(u(t,i[s.children])),l.append(a)):l.append(u(t,i[s.children]))),r.append(l))}),r},a=['
      ',"
      "].join("");!(e="contextmenu"!==d.trigger&&!lay.isTopElem(d.elem[0])?e:!0)&&d.elem.data(y+"_opened")||(l.elemView=c("."+g+'[lay-id="'+d.id+'"]'),"reloadData"===i&&l.elemView.length?l.elemView.html(d.content||n()):(l.elemView=c(a),l.elemView.append(d.content||n()),d.className&&l.elemView.addClass(d.className),d.style&&l.elemView.attr("style",d.style),f.thisId=d.id,l.remove(),t.append(l.elemView),d.elem.data(y+"_opened",!0),e=d.shade?'
      ':"",l.elemView.before(e),"mouseenter"===d.trigger&&l.elemView.on("mouseenter",function(){clearTimeout(h.timer)}).on("mouseleave",function(){l.delayRemove()})),l.position(),(h.prevElem=l.elemView).data("prevElem",d.elem),l.elemView.find(".layui-menu").on(o,function(e){layui.stope(e)}),l.elemView.find(".layui-menu li").on("click",function(e){var i=c(this),t=i.data("item")||{},n=t[s.children]&&0n.width()&&(t.addClass(x),(i=t[0].getBoundingClientRect()).left<0)&&t.removeClass(x),i.bottom>n.height())&&t.eq(0).css("margin-top",-(i.bottom-n.height()+5))}).on("mouseleave",t,function(e){var i=c(this).children("."+C);i.removeClass(x),i.css("margin-top",0)}),f.close=function(e){e=h.getThis(e);return e?(e.remove(),h.call(e)):this},f.reload=function(e,i,t){e=h.getThis(e);return e?(e.reload(i,t),h.call(e)):this},f.reloadData=function(){var t=c.extend([],arguments),n=(t[2]="reloadData",new RegExp("^("+["data","templet","content"].join("|")+")$"));return layui.each(t[1],function(e,i){n.test(e)||delete t[1][e]}),f.reload.apply(null,t)},f.render=function(e){e=new E(e);return h.call(e)},e(r,f)});layui.define(["jquery","lay"],function(e){"use strict";var g=layui.$,c=layui.lay,m={config:{},index:layui.slider?layui.slider.index+1e4:0,set:function(e){var i=this;return i.config=g.extend({},i.config,e),i},on:function(e,i){return layui.onevent.call(this,t,e,i)}},t="slider",v="layui-disabled",x="layui-slider-bar",b="layui-slider-wrap",T="layui-slider-wrap-btn",w="layui-slider-tips",M="layui-slider-input-txt",L="layui-slider-hover",i=function(e){var i=this;i.index=++m.index,i.config=g.extend({},i.config,m.config,e),i.render()};i.prototype.config={type:"default",min:0,max:100,value:0,step:1,showstep:!1,tips:!0,input:!1,range:!1,height:200,disabled:!1,theme:"#16baaa"},i.prototype.render=function(){var a=this,n=a.config,e=g(n.elem);if(1n.max&&(n.value=n.max),t=Math.floor((n.value-n.min)/(n.max-n.min)*100)+"%");var l,e=n.disabled?"#c2c2c2":n.theme,i='
      '+(n.tips?'
      ':"")+'
      '+(n.range?'
      ':"")+"
      ",t=g(n.elem),s=t.next(".layui-slider");if(s[0]&&s.remove(),a.elemTemp=g(i),n.range?(a.elemTemp.find("."+b).eq(0).data("value",n.value[0]),a.elemTemp.find("."+b).eq(1).data("value",n.value[1])):a.elemTemp.find("."+b).data("value",n.value),t.html(a.elemTemp),"vertical"===n.type&&a.elemTemp.height(n.height+"px"),n.showstep){for(var o=(n.max-n.min)/n.step,r="",d=1;d<1+o;d++){var u=100*d/o;u<100&&(r+='
      ')}a.elemTemp.append(r)}n.input&&!n.range&&(e=g('
      '),t.css("position","relative"),t.append(e),t.find("."+M).children("input").val(n.value),"vertical"===n.type?e.css({left:0,top:-48}):a.elemTemp.css("margin-right",e.outerWidth()+15)),n.disabled?(a.elemTemp.addClass(v),a.elemTemp.find("."+T).addClass(v)):a.slide(),a.elemTemp.find("."+T).on("mouseover",function(){var e="vertical"===n.type?n.height:a.elemTemp[0].offsetWidth,i=a.elemTemp.find("."+b),t=("vertical"===n.type?e-g(this).parent()[0].offsetTop-i.height():g(this).parent()[0].offsetLeft)/e*100,i=g(this).parent().data("value"),e=n.setTips?n.setTips(i):i;a.elemTemp.find("."+w).html(e),clearTimeout(l),l=setTimeout(function(){"vertical"===n.type?a.elemTemp.find("."+w).css({bottom:t+"%","margin-bottom":"20px",display:"inline-block"}):a.elemTemp.find("."+w).css({left:t+"%",display:"inline-block"})},300)}).on("mouseout",function(){clearTimeout(l),a.elemTemp.find("."+w).css("display","none")})},i.prototype.slide=function(e,i,t){var o=this,r=o.config,d=o.elemTemp,u=function(){return"vertical"===r.type?r.height:d[0].offsetWidth},c=d.find("."+b),m=d.next(".layui-slider-input"),v=m.children("."+M).children("input").val(),p=100/((r.max-r.min)/Math.ceil(r.step)),f=function(e,i,t){e=(e=100<(e=100a[1]&&a.reverse(),o.value=r.range?a:l,r.change&&r.change(o.value),"done"===t&&r.done&&r.done(o.value)},h=function(e){var i=e/u()*100/p,t=Math.round(i)*p;return t=e==u()?Math.ceil(i)*p:t},y=g(['
      u()?u():i)/u()*100/p;f(i,l),s.addClass(L),d.find("."+w).show(),e.preventDefault()},i=function(){s.removeClass(L),d.find("."+w).hide()},t=function(){i&&i(),y.remove(),r.done&&r.done(o.value)},g("#LAY-slider-moving")[0]||g("body").append(y),y.on("mousemove",e),y.on("mouseup",t).on("mouseleave",t)})}),d.on("click",function(e){var i=g("."+T),t=g(this);!i.is(event.target)&&0===i.has(event.target).length&&i.length&&(t=(i=(i=(i="vertical"===r.type?u()-e.clientY+t.offset().top-g(window).scrollTop():e.clientX-t.offset().left-g(window).scrollLeft())<0?0:i)>u()?u():i)/u()*100/p,i=r.range?"vertical"===r.type?Math.abs(i-parseInt(g(c[0]).css("bottom")))>Math.abs(i-parseInt(g(c[1]).css("bottom")))?1:0:Math.abs(i-c[0].offsetLeft)>Math.abs(i-c[1].offsetLeft)?1:0:0,f(t,i,"done"),e.preventDefault())}),m.children(".layui-slider-input-btn").children("i").each(function(i){g(this).on("click",function(){v=m.children("."+M).children("input").val();var e=((v=1==i?v-r.stepr.max?r.max:Number(v)+r.step)-r.min)/(r.max-r.min)*100/p;f(e,0,"done")})});var a=function(){var e=this.value,e=(e=(e=(e=isNaN(e)?0:e)r.max?r.max:e,((this.value=e)-r.min)/(r.max-r.min)*100/p);f(e,0,"done")};m.children("."+M).children("input").on("keydown",function(e){13===e.keyCode&&(e.preventDefault(),a.call(this))}).on("change",a)},i.prototype.events=function(){this.config},m.render=function(e){e=new i(e);return function(){var t=this,a=t.config;return{setValue:function(e,i){return e=(e=e>a.max?a.max:e)',"",'','',"","","
      "].join("")),r=i.elem=m(i.elem);i.size&&o.addClass("layui-colorpicker-"+i.size),r.addClass("layui-inline").html(e.elemColorBox=o),i.id="id"in i?i.id:r.attr("id")||e.index,e.color=e.elemColorBox.find("."+C)[0].style.background,e.events()},d.prototype.renderPicker=function(){var o,e=this,i=e.config,r=e.elemColorBox[0],t=e.elemPicker=m(['
      ','
      ','
      ','
      ','
      ','
      ',"
      ",'
      ','
      ',"
      ","
      ",'
      ','
      ','
      ',"
      ","
      ",i.predefine?(o=['
      '],layui.each(i.colors,function(e,i){o.push(['
      ','
      ',"
      "].join(""))}),o.push("
      "),o.join("")):"",'
      ','
      ','',"
      ",'
      ','','',"","
      "].join(""));e.elemColorBox.find("."+C)[0];m(a)[0]&&m(a).data("index")==e.index?e.removePicker(d.thisElemInd):(e.removePicker(d.thisElemInd),m("body").append(t)),n.thisId=i.id,d.thisElemInd=e.index,d.thisColor=r.style.background,e.position(),e.pickerEvents()},d.prototype.removePicker=function(e){var i=this.config,e=m("#layui-colorpicker"+(e||this.index));return e[0]&&(e.remove(),delete n.thisId,"function"==typeof i.close)&&i.close(this.color),this},d.prototype.position=function(){var e=this,i=e.config;return t.position(e.bindElem||e.elemColorBox[0],e.elemPicker[0],{position:i.position,align:"center"}),e},d.prototype.val=function(){var e,i=this,o=(i.config,i.elemColorBox.find("."+C)),r=i.elemPicker.find("."+M),t=o[0].style.backgroundColor;t?(e=Y(L(t)),o=o.attr("lay-type"),i.select(e.h,e.s,e.b),"torgb"===o?r.find("input").val(t):"rgba"===o?(o=L(t),3===(t.match(/[0-9]{1,3}/g)||[]).length?(r.find("input").val("rgba("+o.r+", "+o.g+", "+o.b+", 1)"),i.elemPicker.find("."+T).css("left",280)):(r.find("input").val(t),t=280*t.slice(t.lastIndexOf(",")+1,t.length-1),i.elemPicker.find("."+T).css("left",t)),i.elemPicker.find("."+D)[0].style.background="linear-gradient(to right, rgba("+o.r+", "+o.g+", "+o.b+", 0), rgb("+o.r+", "+o.g+", "+o.b+"))"):r.find("input").val("#"+F(e))):(i.select(0,100,100),r.find("input").val(""),i.elemPicker.find("."+D)[0].style.background="",i.elemPicker.find("."+T).css("left",280))},d.prototype.side=function(){var n=this,l=n.config,c=n.elemColorBox.find("."+C),a=c.attr("lay-type"),s=n.elemPicker.find(".layui-colorpicker-side"),e=n.elemPicker.find("."+B),d=n.elemPicker.find("."+I),r=n.elemPicker.find("."+E),f=n.elemPicker.find("."+D),u=n.elemPicker.find("."+T),g=e[0].offsetTop/180*360,p=100-(r[0].offsetTop+3)/180*100,h=(r[0].offsetLeft+3)/260*100,v=Math.round(u[0].offsetLeft/280*100)/100,b=n.elemColorBox.find("."+w),i=n.elemPicker.find(".layui-colorpicker-pre").children("div"),y=function(e,i,o,r){n.select(e,i,o);var t=j({h:e,s:i,b:o}),e=F({h:e,s:i,b:o}),i=n.elemPicker.find("."+M).find("input");b.addClass(x).removeClass(P),c[0].style.background="rgb("+t.r+", "+t.g+", "+t.b+")","torgb"===a?i.val("rgb("+t.r+", "+t.g+", "+t.b+")"):"rgba"===a?(u.css("left",280*r),i.val("rgba("+t.r+", "+t.g+", "+t.b+", "+r+")"),c[0].style.background="rgba("+t.r+", "+t.g+", "+t.b+", "+r+")",f[0].style.background="linear-gradient(to right, rgba("+t.r+", "+t.g+", "+t.b+", 0), rgb("+t.r+", "+t.g+", "+t.b+"))"):i.val("#"+e),l.change&&l.change(n.elemPicker.find("."+M).find("input").val())},o=m(['
      '].join("")),k=function(e){m("#LAY-colorpicker-moving")[0]||m("body").append(o),o.on("mousemove",e),o.on("mouseup",function(){o.remove()}).on("mouseleave",function(){o.remove()})};e.on("mousedown",function(e){var r=this.offsetTop,t=e.clientY;k(function(e){var i=r+(e.clientY-t),o=s[0].offsetHeight,o=(i=o<(i=i<0?0:i)?o:i)/180*360;y(g=o,h,p,v),e.preventDefault()}),e.preventDefault()}),s.on("click",function(e){var i=e.clientY-m(this).offset().top+H.scrollTop(),i=(i=(i=i<0?0:i)>this.offsetHeight?this.offsetHeight:i)/180*360;y(g=i,h,p,v),e.preventDefault()}),r.on("mousedown",function(e){var n=this.offsetTop,l=this.offsetLeft,c=e.clientY,a=e.clientX;layui.stope(e),k(function(e){var i=n+(e.clientY-c),o=l+(e.clientX-a),r=d[0].offsetHeight-3,t=d[0].offsetWidth-3,t=((o=t<(o=o<-3?-3:o)?t:o)+3)/260*100,o=100-((i=r<(i=i<-3?-3:i)?r:i)+3)/180*100;y(g,h=t,p=o,v),e.preventDefault()}),e.preventDefault()}),d.on("mousedown",function(e){var i=e.clientY-m(this).offset().top-3+H.scrollTop(),o=e.clientX-m(this).offset().left-3+H.scrollLeft(),o=((i=i<-3?-3:i)>this.offsetHeight-3&&(i=this.offsetHeight-3),((o=(o=o<-3?-3:o)>this.offsetWidth-3?this.offsetWidth-3:o)+3)/260*100),i=100-(i+3)/180*100;y(g,h=o,p=i,v),layui.stope(e),e.preventDefault(),r.trigger(e,"mousedown")}),u.on("mousedown",function(e){var r=this.offsetLeft,t=e.clientX;k(function(e){var i=r+(e.clientX-t),o=f[0].offsetWidth,o=(o<(i=i<0?0:i)&&(i=o),Math.round(i/280*100)/100);y(g,h,p,v=o),e.preventDefault()}),e.preventDefault()}),f.on("click",function(e){var i=e.clientX-m(this).offset().left,i=((i=i<0?0:i)>this.offsetWidth&&(i=this.offsetWidth),Math.round(i/280*100)/100);y(g,h,p,v=i),e.preventDefault()}),i.each(function(){m(this).on("click",function(){m(this).parent(".layui-colorpicker-pre").addClass("selected").siblings().removeClass("selected");var e=this.style.backgroundColor,i=Y(L(e)),o=e.slice(e.lastIndexOf(",")+1,e.length-1);g=i.h,h=i.s,p=i.b,3===(e.match(/[0-9]{1,3}/g)||[]).length&&(o=1),v=o,y(i.h,i.s,i.b,o)})})},d.prototype.select=function(e,i,o,r){this.config;var t=F({h:e,s:100,b:100}),e=(F({h:e,s:i,b:o}),e/360*180),o=180-o/100*180-3,i=i/100*260-3;this.elemPicker.find("."+B).css("top",e),this.elemPicker.find("."+I)[0].style.background="#"+t,this.elemPicker.find("."+E).css({top:o,left:i})},d.prototype.pickerEvents=function(){var c=this,a=c.config,s=c.elemColorBox.find("."+C),d=c.elemPicker.find("."+M+" input"),o={clear:function(e){s[0].style.background="",c.elemColorBox.find("."+w).removeClass(x).addClass(P),c.color="",a.done&&a.done(""),c.removePicker()},confirm:function(e,i){var o,r,t,n,l=d.val();-1>16,g:(65280&t)>>8,b:255&t},r=Y(n),s[0].style.background=o="#"+F(r),c.elemColorBox.find("."+w).removeClass(P).addClass(x)),"change"===i?(c.select(r.h,r.s,r.b,i),a.change&&a.change(o)):(c.color=l,a.done&&a.done(l),c.removePicker())}};c.elemPicker.on("click","*[colorpicker-events]",function(){var e=m(this),i=e.attr("colorpicker-events");o[i]&&o[i].call(this,e)}),d.on("keyup",function(e){var i=m(this);o.confirm.call(this,i,13===e.keyCode?null:"change")})},d.prototype.events=function(){var e=this;e.config;e.elemColorBox.on("click",function(){e.renderPicker(),m(a)[0]&&(e.val(),e.side())})},s.on(i,function(e){var i,o,r;n.thisId&&(i=l.getThis(n.thisId))&&(o=i.config,r=i.elemColorBox.find("."+C),m(e.target).hasClass(c)||m(e.target).parents("."+c)[0]||m(e.target).hasClass(a.replace(/\./g,""))||m(e.target).parents(a)[0]||i.elemPicker&&(i.color?(e=Y(L(i.color)),i.select(e.h,e.s,e.b)):i.elemColorBox.find("."+w).removeClass(x).addClass(P),r[0].style.background=i.color||"","function"==typeof o.cancel&&o.cancel(i.color),i.removePicker()))}),H.on("resize",function(){if(n.thisId){var e=l.getThis(n.thisId);if(e)return!(!e.elemPicker||!m(a)[0])&&void e.position()}}),l.that={},l.getThis=function(e){var i=l.that[e];return i||o.error(e?r+" instance with ID '"+e+"' not found":"ID argument required"),i},n.render=function(e){e=new d(e);return l.call(e)},e(r,n)});layui.define("jquery",function(t){"use strict";var u=layui.$,d=(layui.hint(),layui.device()),c="element",r="layui-this",h="layui-show",o=".layui-tab-title",i=function(){this.config={}},y=(i.prototype.set=function(t){return u.extend(!0,this.config,t),this},i.prototype.on=function(t,i){return layui.onevent.call(this,c,t,i)},i.prototype.tabAdd=function(t,i){var a,e=u(".layui-tab[lay-filter="+t+"]"),l=e.children(o),n=l.children(".layui-tab-bar"),e=e.children(".layui-tab-content"),s=""+(i.title||"unnaming")+"";return n[0]?n.before(s):l.append(s),e.append('
      '+(i.content||"")+"
      "),i.change&&this.tabChange(t,i.id),l.data("LAY_TAB_CHANGE",i.change),C.tabAuto(i.change?"change":null),this},i.prototype.tabDelete=function(t,i){t=u(".layui-tab[lay-filter="+t+"]").children(o).find('>li[lay-id="'+i+'"]');return C.tabDelete(null,t),this},i.prototype.tabChange=function(t,i){t=u(".layui-tab[lay-filter="+t+"]").children(o).find('>li[lay-id="'+i+'"]');return C.tabClick.call(t[0],{liElem:t}),this},i.prototype.tab=function(a){a=a||{},e.on("click",a.headerElem,function(t){var i=u(this).index();C.tabClick.call(this,{index:i,options:a})})},i.prototype.progress=function(t,i){var a="layui-progress",t=u("."+a+"[lay-filter="+t+"]").find("."+a+"-bar"),a=t.find("."+a+"-text");return t.css("width",function(){return/^.+\/.+$/.test(i)?100*new Function("return "+i)()+"%":i}).attr("lay-percent",i),a.text(i),this},".layui-nav"),f="layui-nav-item",p="layui-nav-bar",b="layui-nav-tree",v="layui-nav-child",m="layui-nav-more",g="layui-anim layui-anim-upbit",C={tabClick:function(t){var i=(t=t||{}).options||{},a=t.liElem||u(this),e=i.headerElem?a.parent():a.parents(".layui-tab").eq(0),i=i.bodyElem?u(i.bodyElem):e.children(".layui-tab-content").children(".layui-tab-item"),l=a.find("a"),l="javascript:;"!==l.attr("href")&&"_blank"===l.attr("target"),n="string"==typeof a.attr("lay-unselect"),s=e.attr("lay-filter"),t="index"in t?t.index:a.parent().children("li").index(a);l||n||(a.addClass(r).siblings().removeClass(r),i.eq(t).addClass(h).siblings().removeClass(h)),layui.event.call(this,c,"tab("+s+")",{elem:e,index:t})},tabDelete:function(t,i){var i=i||u(this).parent(),a=i.parent().children("li").index(i),e=i.closest(".layui-tab"),l=e.children(".layui-tab-content").children(".layui-tab-item"),n=e.attr("lay-filter");i.hasClass(r)&&(i.next()[0]&&i.next().is("li")?C.tabClick.call(i.next()[0],{index:a+1}):i.prev()[0]&&i.prev().is("li")&&C.tabClick.call(i.prev()[0],null,a-1)),i.remove(),l.eq(a).remove(),setTimeout(function(){C.tabAuto()},50),layui.event.call(this,c,"tabDelete("+n+")",{elem:e,index:a})},tabAuto:function(l){var n="layui-tab-more",s="layui-tab-bar",o="layui-tab-close",c=this;u(".layui-tab").each(function(){var t,i=u(this),a=i.children(".layui-tab-title"),e=(i.children(".layui-tab-content").children(".layui-tab-item"),'lay-stope="tabmore"'),e=u('');c===window&&d.ie,i.attr("lay-allowclose")&&a.find("li").each(function(){var t,i=u(this);i.find("."+o)[0]||((t=u('')).on("click",C.tabDelete),i.append(t))}),"string"!=typeof i.attr("lay-unauto")&&(a.prop("scrollWidth")>a.outerWidth()+1||a.find("li").length&&a.height()>(t=a.find("li").eq(0).height())+t/2?("change"===l&&a.data("LAY_TAB_CHANGE")&&a.addClass(n),a.find("."+s)[0]||(a.append(e),i.attr("overflow",""),e.on("click",function(t){var i=a.hasClass(n);a[i?"removeClass":"addClass"](n)}))):(a.find("."+s).remove(),i.removeAttr("overflow")))})},hideTabMore:function(t){var i=u(".layui-tab-title");!0!==t&&"tabmore"===u(t.target).attr("lay-stope")||(i.removeClass("layui-tab-more"),i.find(".layui-tab-bar").attr("title",""))},clickThis:function(){var t=u(this),i=t.closest(y),a=i.attr("lay-filter"),e=t.parent(),l=t.siblings("."+v),n="string"==typeof e.attr("lay-unselect");if("javascript:;"!==t.attr("href")&&"_blank"===t.attr("target")||n||l[0]||(i.find("."+r).removeClass(r),e.addClass(r)),i.hasClass(b)){var n=f+"ed",s=!e.hasClass(n),o=function(){u(this).css({display:""}),i.children("."+p).css({opacity:0})};if(l.is(":animated"))return;l.removeClass(g),l[0]&&(s?(l.slideDown(200,o),e.addClass(n)):(e.removeClass(n),l.show().slideUp(200,o)),"string"!=typeof i.attr("lay-accordion")&&"all"!==i.attr("lay-shrink")||((s=e.siblings("."+n)).removeClass(n),s.children("."+v).show().stop().slideUp(200,o)))}layui.event.call(this,c,"nav("+a+")",t)},collapse:function(){var t=u(this),i=t.find(".layui-colla-icon"),a=t.siblings(".layui-colla-content"),e=t.parents(".layui-collapse").eq(0),l=e.attr("lay-filter"),n="none"===a.css("display");"string"==typeof e.attr("lay-accordion")&&((e=e.children(".layui-colla-item").children("."+h)).siblings(".layui-colla-title").children(".layui-colla-icon").html(""),e.removeClass(h)),a[n?"addClass":"removeClass"](h),i.html(n?"":""),layui.event.call(this,c,"collapse("+l+")",{title:t,content:a,show:n})}},a=(i.prototype.render=i.prototype.init=function(t,i){var a=i?'[lay-filter="'+i+'"]':"",i={tab:function(){C.tabAuto.call({})},nav:function(){var s={},o={},c={},r="layui-nav-title";u(y+a).each(function(t){var i=u(this),a=u(''),e=i.find("."+f);i.find("."+p)[0]||(i.append(a),(i.hasClass(b)?e.find("dd,>."+r):e).on("mouseenter",function(){!function(t,i,a){var e,l=u(this),n=l.find("."+v);i.hasClass(b)?n[0]||(e=l.children("."+r),t.css({top:l.offset().top-i.offset().top,height:(e[0]?e:l).outerHeight(),opacity:1})):(n.addClass(g),n.hasClass("layui-nav-child-c")&&n.css({left:-(n.outerWidth()-l.width())/2}),n[0]?t.css({left:t.position().left+t.width()/2,width:0,opacity:0}):t.css({left:l.position().left+parseFloat(l.css("marginLeft")),top:l.position().top+l.height()-t.height()}),s[a]=setTimeout(function(){t.css({width:n[0]?0:l.width(),opacity:n[0]?0:1})},d.ie&&d.ie<10?0:200),clearTimeout(c[a]),"block"===n.css("display")&&clearTimeout(o[a]),o[a]=setTimeout(function(){n.addClass(h),l.find("."+m).addClass(m+"d")},300))}.call(this,a,i,t)}).on("mouseleave",function(){i.hasClass(b)?a.css({height:0,opacity:0}):(clearTimeout(o[t]),o[t]=setTimeout(function(){i.find("."+v).removeClass(h),i.find("."+m).removeClass(m+"d")},300))}),i.on("mouseleave",function(){clearTimeout(s[t]),c[t]=setTimeout(function(){i.hasClass(b)||a.css({width:0,left:a.position().left+a.width()/2,opacity:0})},200)})),e.find("a").each(function(){var t=u(this);t.parent();t.siblings("."+v)[0]&&!t.children("."+m)[0]&&t.append(''),t.off("click",C.clickThis).on("click",C.clickThis)})})},breadcrumb:function(){u(".layui-breadcrumb"+a).each(function(){var t=u(this),i="lay-separator",a=t.attr(i)||"/",e=t.find("a");e.next("span["+i+"]")[0]||(e.each(function(t){t!==e.length-1&&u(this).after(""+a+"")}),t.css("visibility","visible"))})},progress:function(){var e="layui-progress";u("."+e+a).each(function(){var t=u(this),i=t.find(".layui-progress-bar"),a=i.attr("lay-percent");i.css("width",function(){return/^.+\/.+$/.test(a)?100*new Function("return "+a)()+"%":a}),t.attr("lay-showpercent")&&setTimeout(function(){i.html(''+a+"")},350)})},collapse:function(){u(".layui-collapse"+a).each(function(){u(this).find(".layui-colla-item").each(function(){var t=u(this),i=t.find(".layui-colla-title"),t="none"===t.find(".layui-colla-content").css("display");i.find(".layui-colla-icon").remove(),i.append(''+(t?"":"")+""),i.off("click",C.collapse).on("click",C.collapse)})})}};return i[t]?i[t]():layui.each(i,function(t,i){i()})},new i),e=u(document);u(function(){a.render()}),e.on("click",".layui-tab-title li",C.tabClick),u(window).on("resize",C.tabAuto),t(c,a)});layui.define(["lay","layer"],function(e){"use strict";var x=layui.$,a=layui.lay,i=layui.layer,b=layui.device(),t="upload",r="layui_"+t+"_index",o={config:{},index:layui[t]?layui[t].index+1e4:0,set:function(e){var i=this;return i.config=x.extend({},i.config,e),i},on:function(e,i){return layui.onevent.call(this,t,e,i)}},l=function(){var i=this,e=i.config.id;return{upload:function(e){i.upload.call(i,e)},reload:function(e){i.reload.call(i,e)},config:(l.that[e]=i).config}},u="layui-upload-file",f="layui-upload-form",F="layui-upload-iframe",w="layui-upload-choose",L="UPLOADING",j=function(e){var i=this;i.index=++o.index,i.config=x.extend({},i.config,o.config,e),i.render()};j.prototype.config={accept:"images",exts:"",auto:!0,bindAction:"",url:"",force:"",field:"file",acceptMime:"",method:"post",data:{},drag:!0,size:0,number:0,multiple:!1,text:{"cross-domain":"Cross-domain requests are not supported","data-format-error":"Please return JSON data format","check-error":"",error:"","limit-number":null,"limit-size":null}},j.prototype.reload=function(e){var i=this;i.config=x.extend({},i.config,e),i.render(!0)},j.prototype.render=function(e){var i=this,t=i.config,n=x(t.elem);return 1"].join("")),n=i.elem.next();(n.hasClass(u)||n.hasClass(f))&&n.remove(),b.ie&&b.ie<10&&i.elem.wrap('
      '),e.isFile()?(e.elemFile=i.elem,i.field=i.elem[0].name):i.elem.after(t),b.ie&&b.ie<10&&e.initIE()},j.prototype.initIE=function(){var t,e=this.config,i=x(''),n=x(['
      ',"
      "].join(""));x("#"+F)[0]||x("body").append(i),e.elem.next().hasClass(f)||(this.elemFile.wrap(n),e.elem.next("."+f).append((t=[],layui.each(e.data,function(e,i){i="function"==typeof i?i():i,t.push('')}),t.join(""))))},j.prototype.msg=function(e){return i.msg(e,{icon:2,shift:6})},j.prototype.isFile=function(){var e=this.config.elem[0];if(e)return"input"===e.tagName.toLocaleLowerCase()&&"file"===e.type},j.prototype.preview=function(n){window.FileReader&&layui.each(this.chooseFiles,function(e,i){var t=new FileReader;t.readAsDataURL(i),t.onload=function(){n&&n(e,i,this.result)}})},j.prototype.upload=function(e,i){var t,n,a,o,u=this,f=u.config,c=f.text||{},l=u.elemFile[0],s=function(){return e||u.files||u.chooseFiles||l.files},r=function(){var a=0,o=0,l=s(),r=function(){f.multiple&&a+o===u.fileLength&&"function"==typeof f.allDone&&f.allDone({total:u.fileLength,successful:a,failed:o})},t=function(t){var n=new FormData,i=function(e){t.unified?layui.each(l,function(e,i){delete i[L]}):delete e[L]};if(layui.each(f.data,function(e,i){i="function"==typeof i?i():i,n.append(e,i)}),t.unified)layui.each(l,function(e,i){i[L]||(i[L]=!0,n.append(f.field,i))});else{if(t.file[L])return;n.append(f.field,t.file),t.file[L]=!0}var e={url:f.url,type:"post",data:n,dataType:f.dataType||"json",contentType:!1,processData:!1,headers:f.headers||{},success:function(e){f.unified?a+=u.fileLength:a++,p(t.index,e),r(t.index),i(t.file)},error:function(e){f.unified?o+=u.fileLength:o++,u.msg(c.error||["Upload failed, please try again.","status: "+(e.status||"")+" - "+(e.statusText||"error")].join("
      ")),m(t.index),r(t.index),i(t.file)}};"function"==typeof f.progress&&(e.xhr=function(){var e=x.ajaxSettings.xhr();return e.upload.addEventListener("progress",function(e){var i;e.lengthComputable&&(i=Math.floor(e.loaded/e.total*100),f.progress(i,(f.item||f.elem)[0],e,t.index))}),e}),x.ajax(e)};f.unified?t({unified:!0,index:0}):layui.each(l,function(e,i){t({index:e,file:i})})},d=function(){var n=x("#"+F);u.elemFile.parent().submit(),clearInterval(j.timer),j.timer=setInterval(function(){var e,i=n.contents().find("body");try{e=i.text()}catch(t){u.msg(c["cross-domain"]),clearInterval(j.timer),m()}e&&(clearInterval(j.timer),i.html(""),p(0,e))},30)},p=function(e,i){if(u.elemFile.next("."+w).remove(),l.value="","json"===f.force&&"object"!=typeof i)try{i=JSON.parse(i)}catch(t){return i={},u.msg(c["data-format-error"])}"function"==typeof f.done&&f.done(i,e||0,function(e){u.upload(e)})},m=function(e){f.auto&&(l.value=""),"function"==typeof f.error&&f.error(e||0,function(e){u.upload(e)})},h=f.exts,g=(n=[],layui.each(e||u.chooseFiles,function(e,i){n.push(i.name)}),n),v={preview:function(e){u.preview(e)},upload:function(e,i){var t={};t[e]=i,u.upload(t)},pushFile:function(){return u.files=u.files||{},layui.each(u.chooseFiles,function(e,i){u.files[e]=i}),u.files},resetFile:function(e,i,t){i=new File([i],t);u.files=u.files||{},u.files[e]=i}},y={file:"\u6587\u4ef6",images:"\u56fe\u7247",video:"\u89c6\u9891",audio:"\u97f3\u9891"}[f.accept]||"\u6587\u4ef6",g=0===g.length?l.value.match(/[^\/\\]+\..+/g)||[]:g;if(0!==g.length){switch(f.accept){case"file":layui.each(g,function(e,i){if(h&&!RegExp(".\\.("+h+")$","i").test(escape(i)))return t=!0});break;case"video":layui.each(g,function(e,i){if(!RegExp(".\\.("+(h||"avi|mp4|wma|rmvb|rm|flash|3gp|flv")+")$","i").test(escape(i)))return t=!0});break;case"audio":layui.each(g,function(e,i){if(!RegExp(".\\.("+(h||"mp3|wav|mid")+")$","i").test(escape(i)))return t=!0});break;default:layui.each(g,function(e,i){if(!RegExp(".\\.("+(h||"jpg|png|gif|bmp|jpeg|svg")+")$","i").test(escape(i)))return t=!0})}if(t)return u.msg(c["check-error"]||"\u9009\u62e9\u7684"+y+"\u4e2d\u5305\u542b\u4e0d\u652f\u6301\u7684\u683c\u5f0f"),l.value="";if("choose"!==i&&!f.auto||(f.choose&&f.choose(v),"choose"!==i)){if(u.fileLength=(a=0,y=s(),layui.each(y,function(){a++}),a),f.number&&u.fileLength>f.number)return u.msg("function"==typeof c["limit-number"]?c["limit-number"](f,u.fileLength):"\u540c\u65f6\u6700\u591a\u53ea\u80fd\u4e0a\u4f20: "+f.number+" \u4e2a\u6587\u4ef6
      \u60a8\u5f53\u524d\u5df2\u7ecf\u9009\u62e9\u4e86: "+u.fileLength+" \u4e2a\u6587\u4ef6");if(01024*f.size&&(i=1<=(i=f.size/1024)?i.toFixed(2)+"MB":f.size+"KB",l.value="",o=i)}),o)return u.msg("function"==typeof c["limit-size"]?c["limit-size"](f,o):"\u6587\u4ef6\u5927\u5c0f\u4e0d\u80fd\u8d85\u8fc7 "+o);if(!f.before||!1!==f.before(v))b.ie?(9'+e+"")};a.elem.off("upload.start").on("upload.start",function(){var e=x(this);n.config.item=e,n.elemFile[0].click()}),b.ie&&b.ie<10||a.elem.off("upload.over").on("upload.over",function(){x(this).attr("lay-over","")}).off("upload.leave").on("upload.leave",function(){x(this).removeAttr("lay-over")}).off("upload.drop").on("upload.drop",function(e,i){var t=x(this),i=i.originalEvent.dataTransfer.files||[];t.removeAttr("lay-over"),o(i),a.auto?n.upload():l(i)}),n.elemFile.on("change",function(){var e=this.files||[];0!==e.length&&(o(e),a.auto?n.upload():l(e))}),a.bindAction.off("upload.action").on("upload.action",function(){n.upload()}),a.elem.data(r)||(a.elem.on("click",function(){n.isFile()||x(this).trigger("upload.start")}),a.drag&&a.elem.on("dragover",function(e){e.preventDefault(),x(this).trigger("upload.over")}).on("dragleave",function(e){x(this).trigger("upload.leave")}).on("drop",function(e){e.preventDefault(),x(this).trigger("upload.drop",e)}),a.bindAction.on("click",function(){x(this).trigger("upload.action")}),a.elem.data(r,a.id))},l.that={},l.getThis=function(e){var i=l.that[e];return i||hint.error(e?t+" instance with ID '"+e+"' not found":"ID argument required"),i},o.render=function(e){e=new j(e);return l.call(e)},e(t,o)});layui.define(["lay","layer","util"],function(e){"use strict";var C=layui.$,h=layui.layer,d=layui.util,l=layui.hint(),w=(layui.device(),"form"),o=".layui-form",N="layui-this",T="layui-hide",$="layui-disabled",t=function(){this.config={verify:{required:function(e){if(!/[\S]+/.test(e))return"\u5fc5\u586b\u9879\u4e0d\u80fd\u4e3a\u7a7a"},phone:function(e){if(e&&!/^1\d{10}$/.test(e))return"\u624b\u673a\u53f7\u683c\u5f0f\u4e0d\u6b63\u786e"},email:function(e){if(e&&!/^([a-zA-Z0-9_\.\-])+\@(([a-zA-Z0-9\-])+\.)+([a-zA-Z0-9]{2,4})+$/.test(e))return"\u90ae\u7bb1\u683c\u5f0f\u4e0d\u6b63\u786e"},url:function(e){if(e&&!/^(#|(http(s?)):\/\/|\/\/)[^\s]+\.[^\s]+$/.test(e))return"\u94fe\u63a5\u683c\u5f0f\u4e0d\u6b63\u786e"},number:function(e){if(e&&isNaN(e))return"\u53ea\u80fd\u586b\u5199\u6570\u5b57"},date:function(e){if(e&&!/^(\d{4})[-\/](\d{1}|0\d{1}|1[0-2])([-\/](\d{1}|0\d{1}|[1-2][0-9]|3[0-1]))*$/.test(e))return"\u65e5\u671f\u683c\u5f0f\u4e0d\u6b63\u786e"},identity:function(e){if(e&&!/(^\d{15}$)|(^\d{17}(x|X|\d)$)/.test(e))return"\u8eab\u4efd\u8bc1\u53f7\u683c\u5f0f\u4e0d\u6b63\u786e"}},autocomplete:null}},i=(t.prototype.set=function(e){return C.extend(!0,this.config,e),this},t.prototype.verify=function(e){return C.extend(!0,this.config.verify,e),this},t.prototype.getFormElem=function(e){return C(o+(e?'[lay-filter="'+e+'"]':""))},t.prototype.on=function(e,t){return layui.onevent.call(this,w,e,t)},t.prototype.val=function(e,i){return this.getFormElem(e).each(function(e,t){var a=C(this);layui.each(i,function(e,t){var i,e=a.find('[name="'+e+'"]');e[0]&&("checkbox"===(i=e[0].type)?e[0].checked=t:"radio"===i?e.each(function(){this.checked=this.value==t}):e.val(t))})}),r.render(null,e),this.getValue(e)},t.prototype.getValue=function(e,t){t=t||this.getFormElem(e);var a={},n={},e=t.find("input,select,textarea");return layui.each(e,function(e,t){var i;C(this);t.name=(t.name||"").replace(/^\s*|\s*&/,""),t.name&&(/^.*\[\]$/.test(t.name)&&(i=t.name.match(/^(.*)\[\]$/g)[0],a[i]=0|a[i],i=t.name.replace(/^(.*)\[\]$/,"$1["+a[i]+++"]")),/^(checkbox|radio)$/.test(t.type)&&!t.checked||(n[i||t.name]=t.value))}),n},t.prototype.render=function(e,t){var i=this.config,a=C(o+(t?'[lay-filter="'+t+'"]':"")),n={input:function(e){var e=e||a.find("input,textarea"),t=(i.autocomplete&&e.attr("autocomplete",i.autocomplete),function(e,t){var i=e.val(),a=Number(i),n=Number(e.attr("step"))||1,l=Number(e.attr("min")),r=Number(e.attr("max")),s=Number(e.attr("lay-precision")),o="click"!==t&&""===i,c="init"===t;isNaN(a)||("click"===t&&(a=!!C(this).index()?a-n:a+n),t=function(e){return((e.toString().match(/\.(\d+$)/)||[])[1]||"").length},s=0<=s?s:Math.max(t(n),t(i)),o||(c||r<=(a=a<=l?l:a)&&(a=r),s&&(a=a.toFixed(s)),e.val(a)),e[(a'),e=layui.isArray(i.value)?i.value:[i.value],e=C((a=[],layui.each(e,function(e,t){a.push('')}),a.join(""))),n=(t.append(e),i.split&&t.addClass("layui-input-split"),i.className&&t.addClass(i.className),r.next("."+u)),l=(n[0]&&n.remove(),r.parent().hasClass(o)||r.wrap('
      '),r.next("."+c));l[0]?((n=l.find("."+u))[0]&&n.remove(),l.prepend(t),r.css("padding-right",function(){return(r.closest(".layui-input-group")[0]?0:l.outerWidth())+t.outerWidth()})):(t.addClass(c),r.after(t)),"auto"===i.show&&d(t,r.val()),"function"==typeof i.init&&i.init.call(this,r,i),r.on("input propertychange",function(){var e=this.value;"auto"===i.show&&d(t,e)}),r.on("blur",function(){"function"==typeof i.blur&&i.blur.call(this,r,i)}),e.on("click",function(){var e=r.attr("lay-filter");C(this).hasClass($)||("function"==typeof i.click&&i.click.call(this,r,i),layui.event.call(this,w,"input-affix("+e+")",{elem:r[0],affix:s,options:i}))})},f={eye:{value:"eye-invisible",click:function(e,t){var i="LAY_FORM_INPUT_AFFIX_SHOW",a=e.data(i);e.attr("type",a?"password":"text").data(i,!a),n({value:a?"eye-invisible":"eye"})}},clear:{value:"clear",click:function(e){e.val("").focus(),d(C(this).parent(),null)},show:"auto",disabled:e},number:{value:["up","down"],split:!0,className:"layui-input-number",disabled:r.is("[disabled]"),init:function(e){t.call(this,e,"init")},click:function(e){t.call(this,e,"click")},blur:function(e){t.call(this,e,"blur")}}};n()})},select:function(e){var p,c="\u8bf7\u9009\u62e9",m="layui-form-select",g="layui-select-title",x="layui-select-none",b="",e=e||a.find("select"),k=function(e,t){C(e.target).parent().hasClass(g)&&!t||(C("."+m).removeClass(m+"ed "+m+"up"),p&&b&&p.val(b)),p=null},u=function(a,e,t){var s,r,i,n,o,l,c=C(this),u=a.find("."+g),d=u.find("input"),f=a.find("dl"),h=f.children("dd"),y=f.children("dt"),v=this.selectedIndex;e||(r=c.attr("lay-search"),i=function(){var e=a.offset().top+a.outerHeight()+5-F.scrollTop(),t=f.outerHeight();v=c[0].selectedIndex,a.addClass(m+"ed"),h.removeClass(T),y.removeClass(T),s=null,h.removeClass(N),0<=v&&h.eq(v).addClass(N),e+t>F.height()&&t<=e&&a.addClass(m+"up"),o()},n=function(e){a.removeClass(m+"ed "+m+"up"),d.blur(),s=null,e||l(d.val(),function(e){var t=c[0].selectedIndex;e&&(b=C(c[0].options[t]).html(),0===t&&b===d.attr("placeholder")&&(b=""),d.val(b||""))})},o=function(){var e,t,i=f.children("dd."+N);i[0]&&(e=i.position().top,t=f.height(),i=i.height(),t\u65e0\u5339\u914d\u9879

      '):f.find("."+x).remove()},"keyup"),""===t&&(c.val(""),f.find("."+N).removeClass(N),(c[0].options[0]||{}).value||f.children("dd:eq(0)").addClass(N),f.find("."+x).remove()),o()}).on("blur",function(e){var t=c[0].selectedIndex;p=d,b=C(c[0].options[t]).text(),0===t&&b===d.attr("placeholder")&&(b=""),setTimeout(function(){l(d.val(),function(e){b||d.val("")},"blur")},200)}),h.on("click",function(){var e=C(this),t=e.attr("lay-value"),i=c.attr("lay-filter");return e.hasClass($)||(e.hasClass("layui-select-tips")?d.val(""):(d.val(e.text()),e.addClass(N)),e.siblings().removeClass(N),c.val(t).removeClass("layui-form-danger"),layui.event.call(this,w,"select("+i+")",{elem:c[0],value:t,othis:a}),n(!0)),!1}),a.find("dl>dt").on("click",function(e){return!1}),C(document).off("click",k).on("click",k))};e.each(function(e,t){var i=C(this),a=i.next("."+m),n=this.disabled,l=t.value,r=C(t.options[t.selectedIndex]),t=t.options[0];if("string"==typeof i.attr("lay-ignore"))return i.show();var s,o="string"==typeof i.attr("lay-search"),t=t&&!t.value&&t.innerHTML||c,r=C(['
      ','
      ','','
      ','
      ',(t=i.find("*"),s=[],layui.each(t,function(e,t){var i=t.tagName.toLowerCase();0!==e||t.value||"optgroup"===i?s.push("optgroup"===i?"
      "+t.label+"
      ":'
      '+C.trim(t.innerHTML)+"
      "):s.push('
      '+C.trim(t.innerHTML||c)+"
      ")}),0===s.length&&s.push('
      \u6ca1\u6709\u9009\u9879
      '),s.join("")+"
      "),"
      "].join(""));a[0]&&a.remove(),i.after(r),u.call(this,r,n,o)})},checkbox:function(e){var o={checkbox:["layui-form-checkbox","layui-form-checked","checkbox"],"switch":["layui-form-switch","layui-form-onswitch","switch"],SUBTRA:"layui-icon-indeterminate"},e=e||a.find("input[type=checkbox]");e.each(function(e,t){var i=C(this),a=i.attr("lay-skin")||"primary",n=d.escape(C.trim(t.title||(t.title=i.attr("lay-text")||""))),l=this.disabled,r=o[a]||o.checkbox,s=i.next("."+r[0]);if(s[0]&&s.remove(),i.next("[lay-checkbox]")[0]&&(n=i.next().html()||""),n="switch"===a?n.split("|"):[n],"string"==typeof i.attr("lay-ignore"))return i.show();l=C(['
      ",(s={checkbox:[n[0]?"
      "+n[0]+"
      ":"primary"===a?"":"
      ",''].join(""),"switch":"
      "+((t.checked?n[0]:n[1])||"")+"
      "})[a]||s.checkbox,"
      "].join(""));i.after(l),function(a,n){var l=C(this);a.on("click",function(){var e=C(this),t=l.attr("lay-filter"),e=e.next("*[lay-checkbox]")[0]?e.next().html():l.attr("title")||"",i=l.attr("lay-skin")||"primary",e="switch"===i?e.split("|"):[e];l[0].disabled||(l[0].indeterminate&&(l[0].indeterminate=!1,a.find("."+o.SUBTRA).removeClass(o.SUBTRA).addClass("layui-icon-ok")),l[0].checked?(l[0].checked=!1,a.removeClass(n[1]),"switch"===i&&a.children("div").html(e[1])):(l[0].checked=!0,a.addClass(n[1]),"switch"===i&&a.children("div").html(e[0])),layui.event.call(l[0],w,n[2]+"("+t+")",{elem:l[0],value:l[0].value,othis:a}))})}.call(this,l,r)})},radio:function(e){var r="layui-form-radio",s=["layui-icon-radio","layui-icon-circle"],e=e||a.find("input[type=radio]");e.each(function(e,t){var i=C(this),a=i.next("."+r),n=this.disabled;if("string"==typeof i.attr("lay-ignore"))return i.show();a[0]&&a.remove();n=C(['
      ','',"
      "+(a=d.escape(t.title||""),a=i.next("[lay-radio]")[0]?i.next().html():a)+"
      ","
      "].join(""));i.after(n),function(a){var n=C(this),l="layui-anim-scaleSpring";a.on("click",function(){var e=n[0].name,t=n.parents(o),i=n.attr("lay-filter"),e=t.find("input[name="+e.replace(/(\.|#|\[|\])/g,"\\$1")+"]");n[0].disabled||(layui.each(e,function(){var e=C(this).next("."+r);this.checked=!1,e.removeClass(r+"ed"),e.children(".layui-icon").removeClass(l+" "+s[0]).addClass(s[1])}),n[0].checked=!0,a.addClass(r+"ed"),a.children(".layui-icon").addClass(l+" "+s[0]),layui.event.call(n[0],w,"radio("+i+")",{elem:n[0],value:n[0].value,othis:a}))})}.call(this,n)})}},t=function(){layui.each(n,function(e,t){t()})};return"object"===layui.type(e)?C(e).is(o)?(a=C(e),t()):e.each(function(e,t){var i=C(t);i.closest(o).length&&("SELECT"===t.tagName?n.select(i):"INPUT"===t.tagName&&("checkbox"===(t=t.type)||"radio"===t?n[t](i):n.input(i)))}):e?n[e]?n[e]():l.error('\u4e0d\u652f\u6301\u7684 "'+e+'" \u8868\u5355\u6e32\u67d3'):t(),this},t.prototype.validate=function(e){var u,d=this.config.verify,f="layui-form-danger";return!(e=C(e))[0]||(e.attr("lay-verify")!==undefined||!1!==this.validate(e.find("*[lay-verify]")))&&(layui.each(e,function(e,r){var s=C(this),t=(s.attr("lay-verify")||"").split("|"),o=s.attr("lay-vertype"),c=C.trim(s.val());if(s.removeClass(f),layui.each(t,function(e,t){var i="",a=d[t];if(a){var n="function"==typeof a?i=a(c,r):!a[0].test(c),l="select"===r.tagName.toLowerCase()||/^(checkbox|radio)$/.test(r.type),i=i||a[1];if("required"===t&&(i=s.attr("lay-reqtext")||i),n)return"tips"===o?h.tips(i,"string"!=typeof s.attr("lay-ignore")&&l?s.next():s,{tips:1}):"alert"===o?h.alert(i,{title:"\u63d0\u793a",shadeClose:!0}):/\b(string|number)\b/.test(typeof i)&&h.msg(i,{icon:5,shift:6}),setTimeout(function(){(l?s.next().find("input"):r).focus()},7),s.addClass(f),u=!0}}),u)return u}),!u)},t.prototype.submit=function(e,t){var i={},a=C(this),e="string"==typeof e?e:a.attr("lay-filter"),n=this.getFormElem?this.getFormElem(e):a.parents(o).eq(0),l=n.find("*[lay-verify]");return!!r.validate(l)&&(i=r.getValue(null,n),l={elem:this.getFormElem?window.event&&window.event.target:this,form:(this.getFormElem?n:a.parents("form"))[0],field:i},"function"==typeof t&&t(l),layui.event.call(this,w,"submit("+e+")",l))}),r=new t,t=C(document),F=C(window);C(function(){r.render()}),t.on("reset",o,function(){var e=C(this).attr("lay-filter");setTimeout(function(){r.render(null,e)},50)}),t.on("submit",o,i).on("click","*[lay-submit]",i),e(w,r)});layui.define(["lay","laytpl","laypage","form","util"],function(n){"use strict";var f=layui.$,r=layui.lay,m=layui.laytpl,I=layui.laypage,p=layui.layer,v=layui.form,g=layui.util,b=layui.hint(),x=layui.device(),k={config:{checkName:"LAY_CHECKED",indexName:"LAY_INDEX",numbersName:"LAY_NUM",disabledName:"LAY_DISABLED"},cache:{},index:layui.table?layui.table.index+1e4:0,set:function(e){var t=this;return t.config=f.extend({},t.config,e),t},on:function(e,t){return layui.onevent.call(this,N,e,t)}},w=function(){var a=this,e=a.config,i=e.id||e.index;return{config:e,reload:function(e,t){a.reload.call(a,e,t)},reloadData:function(e,t){k.reloadData(i,e,t)},setColsWidth:function(){a.setColsWidth.call(a)},resize:function(){a.resize.call(a)}}},C=function(e){var t=w.that[e];return t||b.error(e?"The table instance with ID '"+e+"' not found":"ID argument required"),t||null},l=function(e){var t=w.config[e];return t||b.error(e?"The table instance with ID '"+e+"' not found":"ID argument required"),t||null},T=function(e){var t=this.config||{},a=(e=e||{}).item3,i=e.content;"numbers"===a.type&&(i=e.tplData[k.config.numbersName]);("escape"in a?a:t).escape&&(i=g.escape(i));t=e.text&&a.exportTemplet||a.templet||a.toolbar;return t&&(i="function"==typeof t?t.call(a,e.tplData,e.obj):m(f(t).html()||String(i)).render(f.extend({LAY_COL:a},e.tplData))),e.text?f("
      "+i+"
      ").text():i},N="table",t=".layui-table",R="layui-hide",y="layui-hide-v",h="layui-none",D="layui-table-view",o=".layui-table-header",L=".layui-table-body",c=".layui-table-fixed-r",A=".layui-table-pageview",E=".layui-table-sort",_="layui-table-checked",M="layui-table-edit",O="layui-table-hover",P="laytable-cell-group",W="layui-table-col-special",j="layui-table-tool-panel",H="layui-table-expanded",S="LAY_TABLE_MOVE_DICT",e=function(e){return['',"","{{# layui.each(d.data.cols, function(i1, item1){ }}","","{{# layui.each(item1, function(i2, item2){ }}",'{{# if(item2.fixed && item2.fixed !== "right"){ left = true; } }}','{{# if(item2.fixed === "right"){ right = true; } }}',(e=e||{}).fixed&&"right"!==e.fixed?'{{# if(item2.fixed && item2.fixed !== "right"){ }}':"right"===e.fixed?'{{# if(item2.fixed === "right"){ }}':"","{{# var isSort = !(item2.colGroup) && item2.sort; }}",'",e.fixed?"{{# }; }}":"","{{# }); }}","","{{# }); }}","","
      ','
      ','{{# if(item2.type === "checkbox"){ }}','',"{{# } else { }}",'{{-item2.title||""}}',"{{# if(isSort){ }}",'',"{{# } }}","{{# } }}","
      ","
      "].join("")},a=['',"","
      "].join(""),u=[,"{{# if(d.data.toolbar){ }}",'
      ','
      ','
      ',"
      ","{{# } }}",'
      ',"{{# if(d.data.loading){ }}",'
      ','',"
      ","{{# } }}","{{# var left, right; }}",'
      ',e(),"
      ",'
      ',a,"
      ","{{# if(left){ }}",'
      ','
      ',e({fixed:!0}),"
      ",'
      ',a,"
      ","
      ","{{# }; }}","{{# if(right){ }}",'
      ','
      ',e({fixed:"right"}),'
      ',"
      ",'
      ',a,"
      ","
      ","{{# }; }}","
      ","{{# if(d.data.totalRow){ }}",'
      ','','',"
      ","
      ","{{# } }}",'
      ','
      ',"
      "].join(""),d=f(window),F=f(document),i=function(e){this.index=++k.index,this.config=f.extend({},this.config,k.config,e),this.render()},s=(i.prototype.config={limit:10,loading:!0,escape:!0,cellMinWidth:60,cellMaxWidth:Number.MAX_VALUE,editTrigger:"click",defaultToolbar:["filter","exports","print"],defaultContextmenu:!0,autoSort:!0,text:{none:"\u65e0\u6570\u636e"},cols:[]},i.prototype.render=function(e){var t=this,a=t.config,i=(a.elem=f(a.elem),a.where=a.where||{},a.id="id"in a?a.id:a.elem.attr("id")||t.index);if(w.that[i]=t,(w.config[i]=a).request=f.extend({pageName:"page",limitName:"limit"},a.request),a.response=f.extend({statusName:"code",statusCode:0,msgName:"msg",dataName:"data",totalRowName:"totalRow",countName:"count"},a.response),null!==a.page&&"object"==typeof a.page&&(a.limit=a.page.limit||a.limit,a.limits=a.page.limits||a.limits,t.page=a.page.curr=a.page.curr||1,delete a.page.elem,delete a.page.jump),!a.elem[0])return t;if(a.elem.attr("lay-filter")||a.elem.attr("lay-filter",a.id),"reloadData"===e)return t.pullData(t.page,{type:"reloadData"});a.index=t.index,t.key=a.id||a.index,t.setInit(),a.height&&/^full-\d+$/.test(a.height)?(t.fullHeightGap=a.height.split("-")[1],a.height=d.height()-t.fullHeightGap):a.height&&/^#\w+\S*-\d+$/.test(a.height)&&(i=a.height.split("-"),t.parentHeightGap=i.pop(),t.parentDiv=i.join("-"),a.height=f(t.parentDiv).height()-t.parentHeightGap);var l,e=a.elem,i=e.next("."+D),n=t.elem=f("
      ");n.addClass((l=[D,D+"-"+t.index,"layui-form","layui-border-box"],a.className&&l.push(a.className),l.join(" "))).attr({"lay-filter":"LAY-TABLE-FORM-DF-"+t.index,"lay-id":a.id,style:(l=[],a.width&&l.push("width:"+a.width+"px;"),l.join(""))}).html(m(u,{open:"{{",close:"}}"}).render({data:a,index:t.index})),t.renderStyle(),i[0]&&i.remove(),e.after(n),t.layTool=n.find(".layui-table-tool"),t.layBox=n.find(".layui-table-box"),t.layHeader=n.find(o),t.layMain=n.find(".layui-table-main"),t.layBody=n.find(L),t.layFixed=n.find(".layui-table-fixed"),t.layFixLeft=n.find(".layui-table-fixed-l"),t.layFixRight=n.find(c),t.layTotal=n.find(".layui-table-total"),t.layPage=n.find(".layui-table-page"),t.renderToolbar(),t.renderPagebar(),t.fullSize(),t.pullData(t.page),t.events()},i.prototype.initOpts=function(e){this.config;e.checkbox&&(e.type="checkbox"),e.space&&(e.type="space"),e.type||(e.type="normal"),"normal"!==e.type&&(e.unresize=!0,e.width=e.width||{checkbox:50,radio:50,space:30,numbers:60}[e.type])},i.prototype.setInit=function(e){var l,a,d=this,r=d.config;if(r.clientWidth=r.width||(l=function(e){var t,a=(e=e||r.elem.parent()).width();try{t="none"===e.css("display")}catch(i){}return!e[0]||a&&!t?a:l(e.parent())})(),"width"===e)return r.clientWidth;r.height=r.maxHeight||r.height,r.css&&-1===r.css.indexOf(D)&&(a=r.css.split("}"),layui.each(a,function(e,t){t&&(a[e]="."+D+"-"+d.index+" "+t)}),r.css=a.join("}"));var c=function(a,e,i,l){var n,o;l?(l.key=[r.index,a,i].join("-"),l.colspan=l.colspan||0,l.rowspan=l.rowspan||0,d.initOpts(l),(n=a+(parseInt(l.rowspan)||1)) td:hover > .layui-table-cell{overflow: auto;}"].concat(x.ie?[".layui-table-edit{height: "+i+";}","td[data-edit]:hover:after{height: "+i+";}"]:[]),function(e,t){t&&o.push(a+" "+t)})),l.css&&o.push(l.css),r.style({target:this.elem[0],text:o.join(""),id:"DF-table-"+n})},i.prototype.renderToolbar=function(){var e=this.config,t=['
      ','
      ','
      '].join(""),a=this.layTool.find(".layui-table-tool-temp"),i=("default"===e.toolbar?a.html(t):"string"==typeof e.toolbar&&(t=f(e.toolbar).html()||"")&&a.html(m(t).render(e)),{filter:{title:"\u7b5b\u9009\u5217",layEvent:"LAYTABLE_COLS",icon:"layui-icon-cols"},exports:{title:"\u5bfc\u51fa",layEvent:"LAYTABLE_EXPORT",icon:"layui-icon-export"},print:{title:"\u6253\u5370",layEvent:"LAYTABLE_PRINT",icon:"layui-icon-print"}}),l=[];"object"==typeof e.defaultToolbar&&layui.each(e.defaultToolbar,function(e,t){t="string"==typeof t?i[t]:t;t&&l.push('
      ')}),this.layTool.find(".layui-table-tool-self").html(l.join(""))},i.prototype.renderPagebar=function(){var e,t=this.config,a=this.layPagebar=f('
      ');t.pagebar&&((e=f(t.pagebar).html()||"")&&a.append(m(e).render(t)),this.layPage.append(a))},i.prototype.setParentCol=function(e,t){var a=this.config,i=this.layHeader.find('th[data-key="'+t+'"]'),l=parseInt(i.attr("colspan"))||0;i[0]&&(t=t.split("-"),t=a.cols[t[1]][t[2]],e?l--:l++,i.attr("colspan",l),i[l?"removeClass":"addClass"](R),t.colspan2=l,t.hide=l<1,a=i.data("parentkey"))&&this.setParentCol(e,a)},i.prototype.setColsPatch=function(){var a=this,e=a.config;layui.each(e.cols,function(e,t){layui.each(t,function(e,t){t.hide&&a.setParentCol(t.hide,t.parentKey)})})},i.prototype.setGroupWidth=function(i){var e,l=this;l.config.cols.length<=1||((e=l.layHeader.find((i?"th[data-key="+i.data("parentkey")+"]>":"")+"."+P)).css("width",0),layui.each(e.get().reverse(),function(){var e=f(this),t=e.parent().data("key"),a=0;l.layHeader.eq(0).find("th[data-parentkey="+t+"]").width(function(e,t){f(this).hasClass(R)||0 tr:first-child > th:last-child")).data("field")&&e.prev()[0]?t(e.prev()):e})()).data("key"),n.cssRules(e,function(e){var t=e.style.width||a.outerWidth();e.style.width=parseFloat(t)+l+"px",0'+(e||"Error")+"
      ");a[0]&&(t.layNone.remove(),a.remove()),t.layFixed.addClass(R),t.layMain.find("tbody").html(""),t.layMain.append(t.layNone=e),t.layTotal.addClass(y),t.layPage.find(A).addClass(y),k.cache[t.key]=[],t.syncCheckAll(),t.renderForm(),t.setColsWidth()},i.prototype.page=1,i.prototype.pullData=function(a,t){var e,i,l=this,n=l.config,o=(n.HAS_SET_COLS_PATCH||l.setColsPatch(),n.HAS_SET_COLS_PATCH=!0,n.request),d=n.response,r=function(){"object"==typeof n.initSort&&l.sort({field:n.initSort.field,type:n.initSort.type,reloadType:t.type})},c=function(e,t){l.setColsWidth(),"function"==typeof n.done&&n.done(e,a,e[d.countName],t)};t=t||{},"function"==typeof n.before&&n.before(n),l.startTime=(new Date).getTime(),t.renderData?((e={})[d.dataName]=k.cache[l.key],e[d.countName]=n.url?"object"===layui.type(n.page)?n.page.count:e[d.dataName].length:n.data.length,"object"==typeof n.totalRow&&(e[d.totalRowName]=f.extend({},l.totalRow)),l.renderData({res:e,curr:a,count:e[d.countName],type:t.type,sort:!0}),c(e,"renderData")):n.url?(i={},n.page&&(i[o.pageName]=a,i[o.limitName]=n.limit),o=f.extend(i,n.where),n.contentType&&0==n.contentType.indexOf("application/json")&&(o=JSON.stringify(o)),l.loading(),f.ajax({type:n.method||"get",url:n.url,contentType:n.contentType,data:o,dataType:n.dataType||"json",jsonpCallback:n.jsonpCallback,headers:n.headers||{},complete:function(e,t){"function"==typeof n.complete&&n.complete(e,t)},success:function(e){(e="function"==typeof n.parseData?n.parseData(e)||e:e)[d.statusName]!=d.statusCode?l.errorView(e[d.msgName]||'\u8fd4\u56de\u7684\u6570\u636e\u4e0d\u7b26\u5408\u89c4\u8303\uff0c\u6b63\u786e\u7684\u6210\u529f\u72b6\u6001\u7801\u5e94\u4e3a\uff1a"'+d.statusName+'": '+d.statusCode):(l.totalRow=e[d.totalRowName],l.renderData({res:e,curr:a,count:e[d.countName],type:t.type}),r(),n.time=(new Date).getTime()-l.startTime+" ms"),c(e)},error:function(e,t){l.errorView("\u8bf7\u6c42\u5f02\u5e38\uff0c\u9519\u8bef\u63d0\u793a\uff1a"+t),"function"==typeof n.error&&n.error(e,t)}})):"array"===layui.type(n.data)&&(e={},i=a*n.limit-n.limit,o=n.data.concat(),e[d.dataName]=n.page?o.splice(i,n.limit):o,e[d.countName]=n.data.length,"object"==typeof n.totalRow&&(e[d.totalRowName]=f.extend({},n.totalRow)),l.totalRow=e[d.totalRowName],l.renderData({res:e,curr:a,count:e[d.countName],type:t.type}),r(),c(e))},i.prototype.eachCols=function(e){return k.eachCols(null,e,this.config.cols),this},i.prototype.col=function(e){try{return e=e.split("-"),this.config.cols[e[1]][e[2]]||{}}catch(t){return b.error(t),{}}},i.prototype.getTrHtml=function(a,l,n,e){var s=this,u=s.config,y=e&&e.trs||[],h=e&&e.trs_fixed||[],p=e&&e.trs_fixed_r||[];return n=n||1,layui.each(a,function(e,o){var i=[],d=[],r=[],c=e+u.limit*(n-1)+1;if("object"!=typeof o){a[e]=o={LAY_KEY:o};try{k.cache[s.key][e]=o}catch(t){}}"array"===layui.type(o)&&0===o.length||(o[k.config.numbersName]=c,l||(o[k.config.indexName]=e),s.eachCols(function(e,l){var t,e=l.field||e,a=l.key,n=o[e];n!==undefined&&null!==n||(n=""),l.colGroup||(e=['','
      "+function(){var e,t=f.extend(!0,{LAY_COL:l},o),a=k.config.checkName,i=k.config.disabledName;switch(l.type){case"checkbox":return'';case"radio":return'';case"numbers":return c}return l.toolbar?m(f(l.toolbar).html()||"").render(t):T.call(s,{item3:l,content:n,tplData:t})}(),"
      "].join(""),i.push(e),l.fixed&&"right"!==l.fixed&&d.push(e),"right"===l.fixed&&r.push(e))}),e=['data-index="'+e+'"'],o[k.config.checkName]&&e.push('class="'+_+'"'),e=e.join(" "),y.push(""+i.join("")+""),h.push(""+d.join("")+""),p.push(""+r.join("")+""))}),{trs:y,trs_fixed:h,trs_fixed_r:p}},k.getTrHtml=function(e,t){e=C(e);return e.getTrHtml(t,null,e.page)},i.prototype.renderData=function(e){var a=this,i=a.config,t=e.res,l=e.curr,n=a.count=e.count,o=e.sort,d=t[i.response.dataName]||[],t=t[i.response.totalRowName],r=[],c=[],s=[],u=function(){if(!o&&a.sortKey)return a.sort({field:a.sortKey.field,type:a.sortKey.sort,pull:!0,reloadType:e.type});a.getTrHtml(d,o,l,{trs:r,trs_fixed:c,trs_fixed_r:s}),"fixed"===i.scrollPos&&"reloadData"===e.type||a.layBody.scrollTop(0),"reset"===i.scrollPos&&a.layBody.scrollLeft(0),a.layMain.find("."+h).remove(),a.layMain.find("tbody").html(r.join("")),a.layFixLeft.find("tbody").html(c.join("")),a.layFixRight.find("tbody").html(s.join("")),a.syncCheckAll(),a.renderForm(),a.fullSize(),a.haveInit?a.scrollPatch():setTimeout(function(){a.scrollPatch()},50),a.haveInit=!0,p.close(a.tipsIndex)};return k.cache[a.key]=d,a.layTotal[0==d.length?"addClass":"removeClass"](y),a.layPage[i.page||i.pagebar?"removeClass":"addClass"](R),a.layPage.find(A)[!i.page||0==n||0===d.length&&1==l?"addClass":"removeClass"](y),0===d.length?a.errorView(i.text.none):(a.layFixLeft.removeClass(R),o?u():(u(),a.renderTotal(d,t),a.layTotal&&a.layTotal.removeClass(R),void(i.page&&(i.page=f.extend({elem:"layui-table-page"+i.index,count:n,limit:i.limit,limits:i.limits||[10,20,30,40,50,60,70,80,90],groups:3,layout:["prev","page","next","skip","count","limit"],prev:'',next:'',jump:function(e,t){t||(a.page=e.curr,i.limit=e.limit,a.pullData(e.curr))}},i.page),i.page.count=n,I.render(i.page)))))},k.renderData=function(e){e=C(e);e&&e.pullData(e.page,{renderData:!0,type:"reloadData"})},i.prototype.renderTotal=function(e,o){var d,r=this,c=r.config,s={};c.totalRow&&(layui.each(e,function(e,i){"array"===layui.type(i)&&0===i.length||r.eachCols(function(e,t){var e=t.field||e,a=i[e];t.totalRow&&(s[e]=(s[e]||0)+(parseFloat(a)||0))})}),r.dataTotal=[],d=[],r.eachCols(function(e,t){var a,e=t.field||e,i=o&&o[t.field],l="totalRowDecimals"in t?t.totalRowDecimals:2,l=s[e]?parseFloat(s[e]||0).toFixed(l):"",l=(a=t.totalRowText||"",(n={LAY_COL:t})[e]=l,n=t.totalRow&&T.call(r,{item3:t,content:l,tplData:n})||a,i||n),n=(t.field&&r.dataTotal.push({field:t.field,total:f("
      "+l+"
      ").text()}),['','
      "+("string"==typeof(a=t.totalRow||c.totalRow)?m(a).render(f.extend({TOTAL_NUMS:i||s[e],TOTAL_ROW:o||{},LAY_COL:t},t)):l),"
      "].join(""));d.push(n)}),e=r.layTotal.find(".layui-table-patch"),r.layTotal.find("tbody").html(""+d.join("")+(e.length?e.get(0).outerHTML:"")+""))},i.prototype.getColElem=function(e,t){return e.eq(0).find(".laytable-cell-"+t+":eq(0)")},i.prototype.renderForm=function(e){this.config;var t=this.elem.attr("lay-filter");v.render(e,t)},i.prototype.syncCheckAll=function(){var a,e=this,i=e.config,t=e.layHeader.find('input[name="layTableCheckbox"]'),l=k.checkStatus(e.key);t[0]&&(a=l.isAll,e.eachCols(function(e,t){"checkbox"===t.type&&(t[i.checkName]=a)}),t.prop({checked:l.isAll,indeterminate:!l.isAll&&l.data.length}),v.render(t))},i.prototype.setRowActive=function(e,t,a){this.config;e=this.layBody.find('tr[data-index="'+e+'"]');if(t=t||"layui-table-click",a)return e.removeClass(t);e.addClass(t),e.siblings("tr").removeClass(t)},i.prototype.setRowChecked=function(a){var e=this,i=e.config,l=e.layBody.find("tr"+("all"===a.index?"":'[data-index="'+a.index+'"]')),t=(a=f.extend({type:"checkbox"},a),k.cache[e.key]),n="checked"in a,o=function(e){return"radio"===a.type||(n?a.checked:!e)},t=(layui.each(t,function(e,t){"array"===layui.type(t)||t[i.disabledName]||(Number(a.index)===e||"all"===a.index?(e=t[i.checkName]=o(t[i.checkName]),l[e?"addClass":"removeClass"](_),"radio"===a.type&&l.siblings().removeClass(_)):"radio"===a.type&&delete t[i.checkName])}),l.find('input[lay-type="'+({radio:"layTableRadio",checkbox:"layTableCheckbox"}[a.type]||"checkbox")+'"]:not(:disabled)')),d=t.last(),r=d.closest(c);("radio"===a.type&&r.hasClass(R)?t.first():t).prop("checked",o(d.prop("checked"))),e.syncCheckAll(),e.renderForm(a.type)},i.prototype.sort=function(l){var e,t=this,a={},i=t.config,n=i.elem.attr("lay-filter"),o=k.cache[t.key];"string"==typeof(l=l||{}).field&&(d=l.field,t.layHeader.find("th").each(function(e,t){var a=f(this),i=a.data("field");if(i===l.field)return l.field=a,d=i,!1}));try{var d=d||l.field.data("field"),r=l.field.data("key");if(t.sortKey&&!l.pull&&d===t.sortKey.field&&l.type===t.sortKey.sort)return;var c=t.layHeader.find("th .laytable-cell-"+r).find(E);t.layHeader.find("th").find(E).removeAttr("lay-sort"),c.attr("lay-sort",l.type||null),t.layFixed.find("th")}catch(s){b.error("Table modules: sort field '"+d+"' not matched")}t.sortKey={field:d,sort:l.type},i.autoSort&&("asc"===l.type?e=layui.sort(o,d,null,!0):"desc"===l.type?e=layui.sort(o,d,!0,!0):(e=layui.sort(o,k.config.indexName,null,!0),delete t.sortKey,delete i.initSort)),a[i.response.dataName]=e||o,t.renderData({res:a,curr:t.page,count:t.count,sort:!0,type:l.reloadType}),l.fromEvent&&(i.initSort={field:d,type:l.type},layui.event.call(l.field,N,"sort("+n+")",f.extend({config:i},i.initSort)))},i.prototype.loading=function(e){var t=this;t.config.loading&&(e?(t.layInit&&t.layInit.remove(),delete t.layInit,t.layBox.find(".layui-table-init").remove()):(t.layInit=f(['
      ','',"
      "].join("")),t.layBox.append(t.layInit)))},i.prototype.cssRules=function(t,a){var e=this.elem.children("style")[0];r.getStyleRules(e,function(e){if(e.selectorText===".laytable-cell-"+t)return a(e),!0})},i.prototype.fullSize=function(){var e,a,i=this,t=i.config,l=t.height;i.fullHeightGap?(l=d.height()-i.fullHeightGap)<135&&(l=135):i.parentDiv&&i.parentHeightGap&&(l=f(i.parentDiv).height()-i.parentHeightGap)<135&&(l=135),1
      ')).find("div").css({width:a}),e.find("tr").append(t)):e.find(".layui-table-patch").remove()};n(e.layHeader),n(e.layTotal);n=e.layMain.height()-i;e.layFixed.find(L).css("height",t.height()>=n?n:"auto").scrollTop(e.layMain.scrollTop()),e.layFixRight[k.cache[e.key]&&k.cache[e.key].length&&0');a.html(t),u.height&&a.css("max-height",u.height-(s.layTool.outerHeight()||50)),i.find("."+j)[0]||i.append(a),s.renderForm(),a.on("click",function(e){layui.stope(e)}),e.done&&e.done(a,t)};switch(layui.stope(e),F.trigger("table.tool.panel.remove"),p.close(s.tipsIndex),t){case"LAYTABLE_COLS":n({list:(a=[],s.eachCols(function(e,t){t.field&&"normal"==t.type&&a.push('
    • "+(t.fieldTitle||t.title||t.field)+"
    • ").text())+'" lay-filter="LAY_TABLE_TOOL_COLS">')}),a.join("")),done:function(){v.on("checkbox(LAY_TABLE_TOOL_COLS)",function(e){var e=f(e.elem),t=this.checked,a=e.data("key"),i=s.col(a),l=i.hide,e=e.data("parentkey");i.key&&(i.hide=!t,s.elem.find('*[data-key="'+a+'"]')[t?"removeClass":"addClass"](R),l!=i.hide&&s.setParentCol(!t,e),s.resize(),layui.event.call(this,N,"colToggled("+c+")",{col:i,config:u}))})}});break;case"LAYTABLE_EXPORT":if(!l.length)return p.tips("\u5f53\u524d\u8868\u683c\u65e0\u6570\u636e",this,{tips:3});x.ie?p.tips("\u5bfc\u51fa\u529f\u80fd\u4e0d\u652f\u6301 IE\uff0c\u8bf7\u7528 Chrome \u7b49\u9ad8\u7ea7\u6d4f\u89c8\u5668\u5bfc\u51fa",this,{tips:3}):n({list:['
    • \u5bfc\u51fa csv \u683c\u5f0f\u6587\u4ef6
    • ','
    • \u5bfc\u51fa xls \u683c\u5f0f\u6587\u4ef6
    • '].join(""),done:function(e,t){t.on("click",function(){var e=f(this).data("type");k.exportFile.call(s,u.id,null,e)})}});break;case"LAYTABLE_PRINT":if(!l.length)return p.tips("\u5f53\u524d\u8868\u683c\u65e0\u6570\u636e",this,{tips:3});var o=window.open("about:blank","_blank"),d=[""].join(""),r=f(s.layHeader.html());r.append(s.layMain.find("table").html()),r.append(s.layTotal.find("table").html()),r.find("th.layui-table-patch").remove(),r.find("thead>tr>th."+W).filter(function(e,t){return!f(t).children("."+P).length}).remove(),r.find("tbody>tr>td."+W).remove(),o.document.write(d+r.prop("outerHTML")),o.document.close(),layui.device("edg").edg?(o.onafterprint=o.close,o.print()):(o.print(),o.close())}layui.event.call(this,N,"toolbar("+c+")",f.extend({event:t,config:u},{}))}),s.layHeader.on("click","*[lay-event]",function(e){var t=f(this),a=t.attr("lay-event"),t=t.closest("th").data("key"),t=s.col(t);layui.event.call(this,N,"colTool("+c+")",f.extend({event:a,config:u,col:t},{}))}),s.layPagebar.on("click","*[lay-event]",function(e){var t=f(this).attr("lay-event");layui.event.call(this,N,"pagebar("+c+")",f.extend({event:t,config:u},{}))}),e.on("mousemove",function(e){var t=f(this),a=t.offset().left,e=e.clientX-a;t.data("unresize")||w.eventMoveElem||(d.allowResize=t.width()-e<=10,o.css("cursor",d.allowResize?"col-resize":""))}).on("mouseleave",function(){f(this);w.eventMoveElem||o.css("cursor","")}).on("mousedown",function(e){var t,a=f(this);d.allowResize&&(t=a.data("key"),e.preventDefault(),d.offset=[e.clientX,e.clientY],s.cssRules(t,function(e){var t=e.style.width||a.outerWidth();d.rule=e,d.ruleWidth=parseFloat(t),d.minWidth=a.data("minwidth")||u.cellMinWidth,d.maxWidth=a.data("maxwidth")||u.cellMaxWidth}),a.data(S,d),w.eventMoveElem=a)}),w.docEvent||F.on("mousemove",function(e){var t,a;w.eventMoveElem&&(t=w.eventMoveElem.data(S)||{},w.eventMoveElem.data("resizing",1),e.preventDefault(),t.rule)&&(e=t.ruleWidth+e.clientX-t.offset[0],a=w.eventMoveElem.closest("."+D).attr("lay-id"),a=C(a))&&((e=et.maxWidth&&(e=t.maxWidth),t.rule.style.width=e+"px",a.setGroupWidth(w.eventMoveElem),p.close(s.tipsIndex))}).on("mouseup",function(e){var t,a,i,l,n;w.eventMoveElem&&(i=(t=w.eventMoveElem).closest("."+D).attr("lay-id"),a=C(i))&&(i=t.data("key"),l=a.col(i),n=a.config.elem.attr("lay-filter"),d={},o.css("cursor",""),a.scrollPatch(),t.removeData(S),delete w.eventMoveElem,a.cssRules(i,function(e){l.width=parseFloat(e.style.width),layui.event.call(t[0],N,"colResized("+n+")",{col:l,config:a.config})}))}),w.docEvent=!0,e.on("click",function(e){var t=f(this),a=t.find(E),i=a.attr("lay-sort");if(!a[0]||1===t.data("resizing"))return t.removeData("resizing");s.sort({field:t,type:"asc"===i?"desc":"desc"===i?null:"asc",fromEvent:!0})}).find(E+" .layui-edge ").on("click",function(e){var t=f(this),a=t.index(),t=t.parents("th").eq(0).data("field");layui.stope(e),0===a?s.sort({field:t,type:"asc",fromEvent:!0}):s.sort({field:t,type:"desc",fromEvent:!0})}),s.commonMember=function(e){var t=f(this).parents("tr").eq(0).data("index"),r=s.layBody.find('tr[data-index="'+t+'"]'),c=(c=k.cache[s.key]||[])[t]||{},a={tr:r,config:u,data:k.clearCacheKey(c),dataCache:c,index:t,del:function(){k.cache[s.key][t]=[],r.remove(),s.scrollPatch()},update:function(e,d){e=e||{},layui.each(e,function(i,l){var n=r.children('td[data-field="'+i+'"]'),o=n.children(y);c[i]=a.data[i]=l,s.eachCols(function(e,t){var a;t.field==i?(o.html(T.call(s,{item3:t,content:l,tplData:f.extend({LAY_COL:t},c)})),n.data("content",l)):d&&(t.templet||t.toolbar)&&(e=r.children('td[data-field="'+(t.field||e)+'"]'),a=c[t.field],e.children(y).html(T.call(s,{item3:t,content:a,tplData:f.extend({LAY_COL:t},c)})),e.data("content",a))})}),s.renderForm()},setRowChecked:function(e){s.setRowChecked(f.extend({index:t},e))}};return f.extend(a,e)}),a=(s.elem.on("click",'input[name="layTableCheckbox"]+',function(e){var t=f(this),a=t.closest("td"),t=t.prev(),i=(s.layBody.find('input[name="layTableCheckbox"]'),t.parents("tr").eq(0).data("index")),l=t[0].checked,n="layTableAllChoose"===t.attr("lay-filter");t[0].disabled||(n?s.setRowChecked({index:"all",checked:l}):(s.setRowChecked({index:i,checked:l}),layui.stope(e)),layui.event.call(t[0],N,"checkbox("+c+")",r.call(t[0],{checked:l,type:n?"all":"one",getCol:function(){return s.col(a.data("key"))}})))}),s.elem.on("click",'input[lay-type="layTableRadio"]+',function(e){var t=f(this),a=t.closest("td"),t=t.prev(),i=t[0].checked,l=t.parents("tr").eq(0).data("index");if(layui.stope(e),t[0].disabled)return!1;s.setRowChecked({type:"radio",index:l}),layui.event.call(t[0],N,"radio("+c+")",r.call(t[0],{checked:i,getCol:function(){return s.col(a.data("key"))}}))}),s.layBody.on("mouseenter","tr",function(){var e=f(this),t=e.index();e.data("off")||s.layBody.find("tr:eq("+t+")").addClass(O)}).on("mouseleave","tr",function(){var e=f(this),t=e.index();e.data("off")||s.layBody.find("tr:eq("+t+")").removeClass(O)}).on("click","tr",function(e){var t=[".layui-form-checkbox",".layui-form-switch",".layui-form-radio","[lay-unrow]"].join(",");f(e.target).is(t)||f(e.target).closest(t)[0]||a.call(this,"row")}).on("dblclick","tr",function(){a.call(this,"rowDouble")}).on("contextmenu","tr",function(e){u.defaultContextmenu||e.preventDefault(),a.call(this,"rowContextmenu")}),function(e){var t=f(this);t.data("off")||layui.event.call(this,N,e+"("+c+")",r.call(t.children("td")[0]))}),n=function(e,t){var a,i,l,n;(e=f(e)).data("off")||(a=e.data("field"),n=e.data("key"),n=s.col(n),i=e.closest("tr").data("index"),i=k.cache[s.key][i],l=e.children(y),(n="function"==typeof n.edit?n.edit(i):n.edit)&&((n=f("textarea"===n?'':''))[0].value=e.data("content")||i[a]||l.text(),e.find("."+M)[0]||e.append(n),n.focus(),t)&&layui.stope(t))},i=(s.layBody.on("change","."+M,function(){var e=f(this),t=e.parent(),a=this.value,i=e.parent().data("field"),e=e.closest("tr").data("index"),e=k.cache[s.key][e],l=r.call(t[0],{value:a,field:i,oldValue:e[i],td:t,reedit:function(){setTimeout(function(){n(l.td);var e={};e[i]=l.oldValue,l.update(e)})},getCol:function(){return s.col(t.data("key"))}}),e={};e[i]=a,l.update(e),layui.event.call(t[0],N,"edit("+c+")",l)}).on("blur","."+M,function(){f(this).remove()}),s.layBody.on(u.editTrigger,"td",function(e){n(this,e)}).on("mouseenter","td",function(){t.call(this)}).on("mouseleave","td",function(){t.call(this,"hide")}),s.layTotal.on("mouseenter","td",function(){t.call(this)}).on("mouseleave","td",function(){t.call(this,"hide")}),"layui-table-grid-down"),t=function(e){var t=f(this),a=t.children(y);t.data("off")||t.parent().hasClass(H)||(e?t.find(".layui-table-grid-down").remove():!(a.prop("scrollWidth")>a.outerWidth()||0'))},l=function(e){var t=f(this),a=t.parent(),i=a.data("key"),l=s.col(i),n=a.parent().data("index"),a=a.children(y),o="layui-table-cell-c",d=f('');"tips"===(l.expandedMode||u.cellExpandedMode)?s.tipsIndex=p.tips(['
      ',a.html(),"
      ",''].join(""),a[0],{tips:[3,""],time:-1,anim:-1,maxWidth:x.ios||x.android?300:s.elem.width()/2,isOutAnim:!1,skin:"layui-table-tips",success:function(e,t){e.find(".layui-table-tips-c").on("click",function(){p.close(t)})}}):(s.elem.find("."+o).trigger("click"),s.cssRules(i,function(e){var t=e.style.width,a=l.expandedWidth||u.cellExpandedWidth;atr").each(function(i){n.cols[i]=[],f(this).children().each(function(e){var t=f(this),a=t.attr("lay-data"),a=r.options(this,{attr:a?"lay-data":null,errorText:d+(a||t.attr("lay-options"))}),t=f.extend({title:t.text(),colspan:parseInt(t.attr("colspan"))||1,rowspan:parseInt(t.attr("rowspan"))||1},a);n.cols[i].push(t)})}),e.find("tbody>tr")),t=k.render(n);!a.length||o.data||t.config.url||(l=0,k.eachCols(t.config.id,function(e,i){a.each(function(e){n.data[e]=n.data[e]||{};var t=f(this),a=i.field;n.data[e][a]=t.children("td").eq(l).html()}),l++}),t.reloadData({data:n.data}))}),this},w.that={},w.config={},function(a,i,e,l){var n,o;l.colGroup&&(n=0,a++,l.CHILD_COLS=[],o=e+(parseInt(l.rowspan)||1),layui.each(i[o],function(e,t){t.parentKey?t.parentKey===l.key&&(t.PARENT_COL_INDEX=a,l.CHILD_COLS.push(t),s(a,i,o,t)):t.PARENT_COL_INDEX||1<=n&&n==(l.colspan||1)||(t.PARENT_COL_INDEX=a,l.CHILD_COLS.push(t),n+=parseInt(1td').filter('[data-field="'+e+'"]')}}})).replace(/"/g,'""'),n.push(a='"'+a+'"')))}),d.push(n.join(","))}),c&&layui.each(c.dataTotal,function(e,t){r[t.field]||i.push('"'+(t.total||"")+'"')}),o.join(",")+"\r\n"+d.join("\r\n")+"\r\n"+i.join(","))),u.download=(a.title||n.title||"table_"+(n.index||""))+"."+l,document.body.appendChild(u),u.click(),document.body.removeChild(u)},k.getOptions=l,k.hideCol=function(e,l){var n=C(e);n&&("boolean"===layui.type(l)?n.eachCols(function(e,t){var a=t.key,i=n.col(a),t=t.parentKey;i.hide!=l&&(i=i.hide=l,n.elem.find('*[data-key="'+a+'"]')[i?"addClass":"removeClass"](R),n.setParentCol(i,t))}):(l=layui.isArray(l)?l:[l],layui.each(l,function(e,l){n.eachCols(function(e,t){var a,i;l.field===t.field&&(a=t.key,i=n.col(a),t=t.parentKey,"hide"in l)&&i.hide!=l.hide&&(i=i.hide=!!l.hide,n.elem.find('*[data-key="'+a+'"]')[i?"addClass":"removeClass"](R),n.setParentCol(i,t))})})),f("."+j).remove(),n.resize())},k.reload=function(e,t,a,i){if(l(e))return(e=C(e)).reload(t,a,i),w.call(e)},k.reloadData=function(){var a=f.extend([],arguments),i=(a[3]="reloadData",new RegExp("^("+["elem","id","cols","width","height","maxHeight","toolbar","defaultToolbar","className","css","pagebar"].join("|")+")$"));return layui.each(a[1],function(e,t){i.test(e)&&delete a[1][e]}),k.reload.apply(null,a)},k.render=function(e){e=new i(e);return w.call(e)},k.clearCacheKey=function(e){return delete(e=f.extend({},e))[k.config.checkName],delete e[k.config.indexName],delete e[k.config.numbersName],delete e[k.config.disabledName],e},f(function(){k.init()}),n(N,k)});layui.define(["table"],function(e){"use strict";var E=layui.$,x=layui.form,B=layui.table,y=layui.hint(),j={config:{},on:B.on,eachCols:B.eachCols,index:B.index,set:function(e){var t=this;return t.config=E.extend({},t.config,e),t},resize:B.resize,getOptions:B.getOptions,hideCol:B.hideCol,renderData:B.renderData},i=function(){var a=this,e=a.config,n=e.id||e.index;return{config:e,reload:function(e,t){a.reload.call(a,e,t)},reloadData:function(e,t){j.reloadData(n,e,t)}}},P=function(e){var t=i.that[e];return t||y.error(e?"The treeTable instance with ID '"+e+"' not found":"ID argument required"),t||null},F="layui-hide",L=".layui-table-main",q=".layui-table-fixed-l",R=".layui-table-fixed-r",l="layui-table-checked",h="layui-table-tree",Y="LAY_DATA_INDEX",m="LAY_DATA_INDEX_HISTORY",s="LAY_PARENT_INDEX",b="LAY_CHECKBOX_HALF",H="LAY_EXPAND",z="LAY_HAS_EXPANDED",X="LAY_ASYNC_STATUS",n=["all","parent","children","none"],t=function(e){var t=this;t.index=++j.index,t.config=E.extend(!0,{},t.config,j.config,e),t.init(),t.render()},f=function(n,i,e){var l=B.cache[n];layui.each(e||l,function(e,t){var a=t[Y];-1!==a.indexOf("-")&&(l[a]=t),t[i]&&f(n,i,t[i])})},d=function(l,a,e){var d=P(l),r=("reloadData"!==e&&(d.status={expand:{}}),E.extend(!0,{},d.getOptions(),a)),n=r.tree,o=n.customName.children,i=n.customName.id,c=(delete a.hasNumberCol,delete a.hasChecboxCol,delete a.hasRadioCol,B.eachCols(null,function(e,t){"numbers"===t.type?a.hasNumberCol=!0:"checkbox"===t.type?a.hasChecboxCol=!0:"radio"===t.type&&(a.hasRadioCol=!0)},r.cols),a.parseData),u=a.done;r.url?e&&(!c||c.mod)||(a.parseData=function(){var e=this,t=arguments,a=t[0],t=("function"===layui.type(c)&&(a=c.apply(e,t)||t[0]),e.response.dataName);return n.data.isSimpleData&&!n["async"].enable&&(a[t]=d.flatToTree(a[t])),p(a[t],function(e){e[H]=H in e?e[H]:e[i]!==undefined&&d.status.expand[e[i]]},o),e.autoSort&&e.initSort&&e.initSort.type&&layui.sort(a[t],e.initSort.field,"desc"===e.initSort.type,!0),d.initData(a[t]),a},a.parseData.mod=!0):(a.data=a.data||[],n.data.isSimpleData&&(a.data=d.flatToTree(a.data)),d.initData(a.data)),e&&(!u||u.mod)||(a.done=function(){var e,t=arguments,a=t[3],n=(a||delete d.isExpandAll,this.elem.next()),i=(d.updateStatus(null,{LAY_HAS_EXPANDED:!1}),f(l,o),n.find('[name="layTableCheckbox"][lay-filter="layTableAllChoose"]'));if(i.length&&(e=j.checkStatus(l),i.prop({checked:e.isAll&&e.data.length,indeterminate:!e.isAll&&e.data.length})),!a&&r.autoSort&&r.initSort&&r.initSort.type&&j.sort(l),d.renderTreeTable(n),"function"===layui.type(u))return u.apply(this,t)},a.done.mod=!0)};t.prototype.init=function(){var e=this.config,t=e.tree.data.cascade,t=(-1===n.indexOf(t)&&(e.tree.data.cascade="all"),B.render(E.extend({},e,{data:[],url:"",done:null}))),a=t.config.id;(i.that[a]=this).tableIns=t,d(a,e)},t.prototype.config={tree:{customName:{children:"children",isParent:"isParent",name:"name",id:"id",pid:"parentId",icon:"icon"},view:{indent:14,flexIconClose:'',flexIconOpen:'',showIcon:!0,icon:"",iconClose:'',iconOpen:'',iconLeaf:'',showFlexIconIfNotParent:!1,dblClickExpand:!0,expandAllDefault:!1},data:{isSimpleData:!1,rootPid:null,cascade:"all"},"async":{enable:!1,url:"",type:null,contentType:null,headers:null,where:null,autoParam:[]},callback:{beforeExpand:null,onExpand:null}}},t.prototype.getOptions=function(){return this.tableIns?B.getOptions(this.tableIns.config.id):this.config},t.prototype.flatToTree=function(e){var a,n,i,t,l,d,r,o=this.getOptions(),c=o.tree,u=c.customName,o=o.id;return e=e||B.cache[o],o=e,a=u.id,n=u.pid,i=u.children,t=c.data.rootPid,a=a||"id",n=n||"parentId",i=i||"children",r={},layui.each(o,function(e,t){l=a+t[a],r[l]=E.extend({},t),r[l][i]=[]}),layui.each(r,function(e,t){(d=a+t[n])&&r[d]&&r[d][i].push(t)}),Object.keys(r).map(function(e){return r[e]}).filter(function(e){return t?e[n]===t:!e[n]})},t.prototype.treeToFlat=function(e,n,i){var l=this,d=l.getOptions().tree.customName,r=d.children,o=d.pid,c=[];return layui.each(e,function(e,t){var e=(i?i+"-":"")+e,a=E.extend({},t);a[o]=t[o]||n,c.push(a),c=c.concat(l.treeToFlat(t[r],t[d.id],e))}),c},t.prototype.getTreeNode=function(e){var t,a,n=this;return e?(a=(t=n.getOptions()).tree,t.id,a.customName,{data:e,dataIndex:e[Y],getParentNode:function(){return n.getNodeByIndex(e[s])}}):y.error("\u627e\u4e0d\u5230\u8282\u70b9\u6570\u636e")},t.prototype.getNodeByIndex=function(t){var a,e,n=this,i=n.getNodeDataByIndex(t);return i?((e=n.getOptions()).tree.customName.parent,a=e.id,(e={data:i,dataIndex:i[Y],getParentNode:function(){return n.getNodeByIndex(i[s])},update:function(e){return j.updateNode(a,t,e)},remove:function(){return j.removeNode(a,t)},expand:function(e){return j.expandNode(a,E.extend({},e,{index:t}))},setChecked:function(e){return j.setRowChecked(a,E.extend({},e,{index:t}))}}).dataIndex=t,e):y.error("\u627e\u4e0d\u5230\u8282\u70b9\u6570\u636e")},t.prototype.getNodeById=function(a){var e=this.getOptions(),n=e.tree.customName.id,i="",e=j.getData(e.id,!0);if(layui.each(e,function(e,t){if(t[n]===a)return i=t[Y],!0}),i)return this.getNodeByIndex(i)},t.prototype.getNodeDataByIndex=function(e,t,a){var n=this.getOptions(),i=n.tree,n=n.id,n=B.cache[n],l=n[e];if("delete"!==a&&l)return E.extend(l,a),t?E.extend({},l):l;for(var d=n,r=String(e).split("-"),o=0,c=i.customName.children;o
      '),N=function(e){y[X]="success",y[s.children]=e,c.initData(y[s.children],y[Y]),U(t,!0,!p&&n,i,l)},C=m.format,"function"===layui.type(C)?C(y,o,N):(I=E.extend({},m.where||o.where),C=m.autoParam,layui.each(C,function(e,t){t=t.split("=");I[t[0].trim()]=y[(t[1]||t[0]).trim()]}),(C=m.contentType||o.contentType)&&0==C.indexOf("application/json")&&(I=JSON.stringify(I)),w=m.method||o.method,D=m.dataType||o.dataType,T=m.jsonpCallback||o.jsonpCallback,k=m.headers||o.headers,_=m.parseData||o.parseData,O=m.response||o.response,E.ajax({type:w||"get",url:b,contentType:C,data:I,dataType:D||"json",jsonpCallback:T,headers:k||{},success:function(e){(e="function"==typeof _?_.call(o,e)||e:e)[O.statusName]!=O.statusCode?(y[X]="error",g.html('')):N(e[O.dataName])},error:function(e,t){y[X]="error","function"==typeof o.error&&o.error(e,t)}})),h;y[z]=!0,v.length&&(!o.initSort||o.url&&!o.autoSort||((m=o.initSort).type?layui.sort(v,m.field,"desc"===m.type,!0):layui.sort(v,B.config.indexName,null,!0)),c.initData(y[s.children],y[Y]),w=B.getTrHtml(r,v,null,null,e),S={trs:E(w.trs.join("")),trs_fixed:E(w.trs_fixed.join("")),trs_fixed_r:E(w.trs_fixed_r.join(""))},A=(e.split("-").length-1||0)+1,layui.each(v,function(e,t){S.trs.eq(e).attr({"data-index":t[Y],"lay-data-index":t[Y],"data-level":A}),S.trs_fixed.eq(e).attr({"data-index":t[Y],"lay-data-index":t[Y],"data-level":A}),S.trs_fixed_r.eq(e).attr({"data-index":t[Y],"lay-data-index":t[Y],"data-level":A})}),d.find(L).find('tbody tr[lay-data-index="'+e+'"]').after(S.trs),d.find(q).find('tbody tr[lay-data-index="'+e+'"]').after(S.trs_fixed),d.find(R).find('tbody tr[lay-data-index="'+e+'"]').after(S.trs_fixed_r),c.renderTreeTable(S.trs,A),n)&&!p&&layui.each(v,function(e,t){U({dataIndex:t[Y],trElem:d.find('tr[lay-data-index="'+t[Y]+'"]').first(),tableViewElem:d,tableId:r,options:o},a,n,i,l)})}else c.isExpandAll=!1,(n&&!p?(layui.each(v,function(e,t){U({dataIndex:t[Y],trElem:d.find('tr[lay-data-index="'+t[Y]+'"]').first(),tableViewElem:d,tableId:r,options:o},a,n,i,l)}),d.find(v.map(function(e,t,a){return'tr[lay-data-index="'+e[Y]+'"]'}).join(","))):(b=c.treeToFlat(v,y[s.id],e),d.find(b.map(function(e,t,a){return'tr[lay-data-index="'+e[Y]+'"]'}).join(",")))).addClass(F);V("resize-"+r,function(){j.resize(r)},0)(),l&&"loading"!==y[X]&&(C=u.callback.onExpand,"function"===layui.type(C))&&C(r,y,x)}return h},g=(j.expandNode=function(e,t){var a,n,i,e=P(e);if(e)return a=(t=t||{}).index,n=t.expandFlag,i=t.inherit,t=t.callbackFlag,e=e.getOptions().elem.next(),U({trElem:e.find('tr[lay-data-index="'+a+'"]').first()},n,i,null,t)},j.expandAll=function(a,e){if("boolean"!==layui.type(e))return y.error("expandAll \u7684\u5c55\u5f00\u72b6\u6001\u53c2\u6570\u53ea\u63a5\u6536true/false");var t=P(a);if(t){t.isExpandAll=e;var n=t.getOptions(),i=n.tree,l=n.elem.next(),d=i.customName.isParent,r=i.customName.id,o=i.view.showFlexIconIfNotParent;if(e){e=j.getData(a,!0);if(i["async"].enable){var c=!0;if(layui.each(e,function(e,t){if(t[d]&&!t[X])return!(c=!1)}),!c)return void layui.each(j.getData(a),function(e,t){j.expandNode(a,{index:t[Y],expandFlag:!0,inherit:!0})})}var u=!0;if(layui.each(e,function(e,t){if(t[d]&&!t[z])return!(u=!1)}),u)t.updateStatus(null,function(e){(e[d]||o)&&(e[H]=!0,e[r]!==undefined)&&(t.status.expand[e[r]]=!0)}),l.find('tbody tr[data-level!="0"]').removeClass(F),l.find(".layui-table-tree-flexIcon").html(i.view.flexIconOpen),i.view.showIcon&&l.find(".layui-table-tree-nodeIcon:not(.layui-table-tree-iconCustom,.layui-table-tree-iconLeaf)").html(i.view.iconOpen);else{if(t.updateStatus(null,function(e){(e[d]||o)&&(e[H]=!0,e[z]=!0,e[r]!==undefined)&&(t.status.expand[e[r]]=!0)}),n.initSort&&n.initSort.type&&n.autoSort)return j.sort(a);var s,n=B.getTrHtml(a,e),f={trs:E(n.trs.join("")),trs_fixed:E(n.trs_fixed.join("")),trs_fixed_r:E(n.trs_fixed_r.join(""))};layui.each(e,function(e,t){var a=t[Y].split("-").length-1;s={"data-index":t[Y],"lay-data-index":t[Y],"data-level":a},f.trs.eq(e).attr(s),f.trs_fixed.eq(e).attr(s),f.trs_fixed_r.eq(e).attr(s)}),layui.each(["main","fixed-l","fixed-r"],function(e,t){l.find(".layui-table-"+t+" tbody").html(f[["trs","trs_fixed","trs_fixed_r"][e]])}),t.renderTreeTable(l,0,!1)}}else t.updateStatus(null,function(e){(e[d]||o)&&(e[H]=!1,e[r]!==undefined)&&(t.status.expand[e[r]]=!1)}),l.find('.layui-table-box tbody tr[data-level!="0"]').addClass(F),l.find(".layui-table-tree-flexIcon").html(i.view.flexIconClose),i.view.showIcon&&l.find(".layui-table-tree-nodeIcon:not(.layui-table-tree-iconCustom,.layui-table-tree-iconLeaf)").html(i.view.iconClose);j.resize(a)}},t.prototype.renderTreeTable=function(e,t,a){var n=this,i=n.getOptions(),l=i.elem.next(),d=(l.hasClass(h)||l.addClass(h),i.id),r=i.tree||{},o=(r.data,r.view||{}),c=r.customName||{},u=c.isParent,s=(l.attr("lay-filter"),n),f=((t=t||0)||(l.find(".layui-table-body tr:not([data-level])").attr("data-level",t),layui.each(B.cache[d],function(e,t){l.find('.layui-table-main tbody tr[data-level="0"]:eq('+e+")").attr("lay-data-index",t[Y]),l.find('.layui-table-fixed-l tbody tr[data-level="0"]:eq('+e+")").attr("lay-data-index",t[Y]),l.find('.layui-table-fixed-r tbody tr[data-level="0"]:eq('+e+")").attr("lay-data-index",t[Y])})),null),y=c.name,p=o.indent||14;if(layui.each(e.find('td[data-field="'+y+'"]'),function(e,t){var a,n,i=(t=E(t)).closest("tr"),t=t.children(".layui-table-cell");t.hasClass("layui-table-tree-item")||(n=i.attr("lay-data-index"))&&(i=l.find('tr[lay-data-index="'+n+'"]'),(a=s.getNodeDataByIndex(n))[H]&&a[u]&&((f=f||{})[n]=!0),a[b]&&i.find('input[type="checkbox"][name="layTableCheckbox"]').prop("indeterminate",!0),n=t.html(),(t=i.find('td[data-field="'+y+'"]>div.layui-table-cell')).addClass("layui-table-tree-item"),t.html(['
      ',a[H]?o.flexIconOpen:o.flexIconClose,"
      ",o.showIcon?'
      '+(a[c.icon]||o.icon||(a[u]?a[H]?o.iconOpen:o.iconClose:o.iconLeaf)||"")+"
      ":"",n].join("")).find(".layui-table-tree-flexIcon").on("click",function(e){layui.stope(e),U({trElem:i},null,null,null,!0)}))}),!t&&r.view.expandAllDefault&&n.isExpandAll===undefined)return j.expandAll(d,!0);!1!==a&&f?layui.each(f,function(e,t){e=l.find('tr[lay-data-index="'+e+'"]');e.find(".layui-table-tree-flexIcon").html(o.flexIconOpen),U({trElem:e.first()},!0)}):V("renderTreeTable-"+d,function(){i.hasNumberCol&&g(n),x.render(E('.layui-table-tree[lay-id="'+d+'"]'))},0)()},function(a){var e=a.getOptions(),t=e.elem.next(),n=0,i=t.find(".layui-table-main tbody tr"),l=t.find(".layui-table-fixed-l tbody tr"),d=t.find(".layui-table-fixed-r tbody tr");layui.each(a.treeToFlat(B.cache[e.id]),function(e,t){t.LAY_HIDE||(a.getNodeDataByIndex(t[Y]).LAY_NUM=++n,i.eq(e).find(".laytable-cell-numbers").html(n),l.eq(e).find(".laytable-cell-numbers").html(n),d.eq(e).find(".laytable-cell-numbers").html(n))})}),p=(t.prototype.render=function(e){var t=this;t.tableIns=B["reloadData"===e?"reloadData":"reload"](t.tableIns.config.id,E.extend(!0,{},t.config)),t.config=t.tableIns.config},t.prototype.reload=function(e,t,a){var n=this;e=e||{},delete n.haveInit,layui.each(e,function(e,t){"array"===layui.type(t)&&delete n.config[e]}),d(n.getOptions().id,e,a||!0),n.config=E.extend(t,{},n.config,e),n.render(a)},j.reloadData=function(){var e=E.extend(!0,[],arguments);return e[3]="reloadData",j.reload.apply(null,e)},function(e,a,n,i){var l=[];return layui.each(e,function(e,t){"function"===layui.type(a)?a(t):E.extend(t,a),l.push(E.extend({},t)),i||(l=l.concat(p(t[n],a,n,i)))}),l}),o=(t.prototype.updateStatus=function(e,t,a){var n=this.getOptions(),i=n.tree;return e=e||B.cache[n.id],p(e,t,i.customName.children,a)},t.prototype.getTableData=function(){var e=this.getOptions();return B.cache[e.id]},j.updateStatus=function(e,t,a){var e=P(e),n=e.getOptions();return a=a||(n.url?B.cache[n.id]:n.data),e.updateStatus(a,t)},j.sort=function(e){var t=P(e);t&&t.getOptions().autoSort&&(t.initData(),j.renderData(e))},function(n){var t=n.config.id,i=P(t),a=n.data=j.getNodeDataByIndex(t,n.index),l=a[Y],d=(n.dataIndex=l,n.update);n.update=function(){var e=arguments,t=(E.extend(i.getNodeDataByIndex(l),e[0]),d.apply(this,e)),a=n.config.tree.customName.name;return a in e[0]&&n.tr.find('td[data-field="'+a+'"]').children("div.layui-table-cell").removeClass("layui-table-tree-item"),i.renderTreeTable(n.tr,n.tr.attr("data-level"),!1),t},n.del=function(){j.removeNode(t,a)},n.setRowChecked=function(e){j.setRowChecked(t,{index:a,checked:e})}}),u=(j.updateNode=function(e,a,t){var n,i,l,d,r,o=P(e);o&&((d=o.getOptions()).tree,d=(n=d.elem.next()).find('tr[lay-data-index="'+a+'"]'),i=d.attr("data-index"),l=d.attr("data-level"),t)&&(d=o.getNodeDataByIndex(a,!1,t),r=B.getTrHtml(e,[d]),layui.each(["main","fixed-l","fixed-r"],function(e,t){n.find(".layui-table-"+t+' tbody tr[lay-data-index="'+a+'"]').replaceWith(E(r[["trs","trs_fixed","trs_fixed_r"][e]].join("")).attr({"data-index":i,"lay-data-index":a,"data-level":l}))}),o.renderTreeTable(n.find('tr[lay-data-index="'+a+'"]'),l))},j.removeNode=function(e,t){var a,n,i,l,d,r=P(e);r&&(d=(a=r.getOptions()).tree,n=a.elem.next(),i=[],t=r.getNodeDataByIndex("string"===layui.type(t)?t:t[Y],!1,"delete"),l=r.getNodeDataByIndex(t[s]),r.updateCheckStatus(l),l=r.treeToFlat([t],t[d.customName.pid],t[s]),layui.each(l,function(e,t){i.push('tr[lay-data-index="'+t[Y]+'"]')}),n.find(i.join(",")).remove(),d=r.initData(),layui.each(r.treeToFlat(d),function(e,t){t[m]&&t[m]!==t[Y]&&n.find('tr[lay-data-index="'+t[m]+'"]').attr({"data-index":t[Y],"lay-data-index":t[Y]})}),layui.each(B.cache[e],function(e,t){n.find('tr[data-level="0"][lay-data-index="'+t[Y]+'"]').attr("data-index",e)}),a.hasNumberCol&&g(r),j.resize(e))},j.addNodes=function(e,t){var a=P(e);if(a){var n=a.getOptions(),i=n.tree,l=n.elem.next(),d=B.config.checkName,r=(t=t||{}).parentIndex,o=t.index,c=t.data,t=t.focus,u=(r="number"===layui.type(r)?r.toString():r)?a.getNodeDataByIndex(r):null,o="number"===layui.type(o)?o:-1,c=E.extend(!0,[],layui.isArray(c)?c:[c]);layui.each(c,function(e,t){d in t||!u||(t[d]=u[d])}),a.getTableData();if(u){var s=i.customName.isParent,f=i.customName.children;u[s]=!0;var y=(y=u[f])?(p=y.splice(-1===o?y.length:o),u[f]=y.concat(c,p)):u[f]=c,f=(a.updateStatus(y,function(e){(e[s]||i.view.showFlexIconIfNotParent)&&(e[z]=!1)}),a.treeToFlat(y));l.find(f.map(function(e){return'tr[lay-data-index="'+e[Y]+'"]'}).join(",")).remove(),a.initData(),u[z]=!1,u[X]="local",U({trElem:l.find('tr[lay-data-index="'+r+'"]')},!0)}else{var p=B.cache[e].splice(-1===o?B.cache[e].length:o);if(B.cache[e]=B.cache[e].concat(c,p),n.url||(n.page?(y=n.page,n.data.splice.apply(n.data,[y.limit*(y.curr-1),y.limit].concat(B.cache[e]))):n.data=B.cache[e]),a.initData(),l.find(".layui-none").length)return B.renderData(e),c;var x,f=B.getTrHtml(e,c),h={trs:E(f.trs.join("")),trs_fixed:E(f.trs_fixed.join("")),trs_fixed_r:E(f.trs_fixed_r.join(""))},r=(layui.each(c,function(e,t){x={"data-index":t[Y],"lay-data-index":t[Y],"data-level":"0"},h.trs.eq(e).attr(x),h.trs_fixed.eq(e).attr(x),h.trs_fixed_r.eq(e).attr(x)}),parseInt(c[0][Y])-1),y=l.find(L),n=l.find(q),f=l.find(R);-1==r?(y.find('tr[data-level="0"][data-index="0"]').before(h.trs),n.find('tr[data-level="0"][data-index="0"]').before(h.trs_fixed),f.find('tr[data-level="0"][data-index="0"]').before(h.trs_fixed_r)):-1===o?(y.find("tbody").append(h.trs),n.find("tbody").append(h.trs_fixed),f.find("tbody").append(h.trs_fixed_r)):(r=p[0][m],y.find('tr[data-level="0"][data-index="'+r+'"]').before(h.trs),n.find('tr[data-level="0"][data-index="'+r+'"]').before(h.trs_fixed),f.find('tr[data-level="0"][data-index="'+r+'"]').before(h.trs_fixed_r)),layui.each(B.cache[e],function(e,t){l.find('tr[data-level="0"][lay-data-index="'+t[Y]+'"]').attr("data-index",e)}),a.renderTreeTable(l.find(c.map(function(e,t,a){return'tr[lay-data-index="'+e[Y]+'"]'}).join(",")))}return a.updateCheckStatus(u),j.resize(e),t&&l.find(L).find('tr[lay-data-index="'+c[0][Y]+'"]').get(0).scrollIntoViewIfNeeded(),c}},j.checkStatus=function(e,n){var i,t,a,l=P(e);if(l)return l=l.getOptions().tree,i=B.config.checkName,t=j.getData(e,!0).filter(function(e,t,a){return e[i]||n&&e[b]}),a=!0,layui.each("all"===l.data.cascade?B.cache[e]:j.getData(e,!0),function(e,t){if(!t[i])return!(a=!1)}),{data:t,isAll:a}},j.on("sort",function(e){var e=e.config,t=e.elem.next(),e=e.id;t.hasClass(h)&&j.sort(e)}),j.on("row",function(e){e.config.elem.next().hasClass(h)&&o(e)}),j.on("rowDouble",function(e){var t=e.config,a=t.elem.next();t.id;a.hasClass(h)&&(o(e),(t.tree||{}).view.dblClickExpand)&&U({trElem:e.tr.first()},null,null,null,!0)}),j.on("rowContextmenu",function(e){var t=e.config,a=t.elem.next();t.id;a.hasClass(h)&&o(e)}),j.on("tool",function(e){var t=e.config,a=t.elem.next();t.id;a.hasClass(h)&&o(e)}),j.on("edit",function(e){var t=e.config,a=t.elem.next();t.id;a.hasClass(h)&&(o(e),e.field===t.tree.customName.name)&&((a={})[e.field]=e.value,e.update(a))}),j.on("radio",function(e){var t=e.config,a=t.elem.next(),t=t.id;a.hasClass(h)&&(a=P(t),o(e),u.call(a,e.tr,e.checked))}),t.prototype.setRowCheckedClass=function(e,t){var a=this.getOptions(),n=(e.data("index"),a.elem.next());e[t?"addClass":"removeClass"](l),e.each(function(){var e=E(this).data("index");n.find('.layui-table-fixed-r tbody tr[data-index="'+e+'"]')[t?"addClass":"removeClass"](l)})},t.prototype.updateCheckStatus=function(e,t){var a,n,i,l,d,r,o,c=this,u=c.getOptions();return!!u.hasChecboxCol&&(a=u.tree,n=u.id,i=u.elem.next(),l=B.config.checkName,"all"!==(d=a.data.cascade)&&"parent"!==d||!e||(d=c.updateParentCheckStatus(e,"boolean"===layui.type(t)?t:null),layui.each(d,function(e,t){var a=i.find('tr[lay-data-index="'+t[Y]+'"] input[name="layTableCheckbox"]:not(:disabled)'),n=t[l];c.setRowCheckedClass(a.closest("tr"),n),x.render(a.prop({checked:n,indeterminate:t[b]}))})),o=!(r=!0),e=(e="all"===a.data.cascade?B.cache[n]:j.getData(n,!0)).filter(function(e){return!e[u.disabledName]}),layui.each(e,function(e,t){if((t[l]||t[b])&&(o=!0),t[l]||(r=!1),o&&!r)return!0}),o=o&&!r,x.render(i.find('input[name="layTableCheckbox"][lay-filter="layTableAllChoose"]').prop({checked:r,indeterminate:o})),r)},t.prototype.updateParentCheckStatus=function(a,n){var i,e=this.getOptions(),t=e.tree,e=e.id,l=B.config.checkName,t=t.customName.children,d=[];return!(a[b]=!1)===n?a[t].length?layui.each(a[t],function(e,t){if(!t[l])return n=!1,a[b]=!0}):n=!1:!1===n?layui.each(a[t],function(e,t){if(t[l]||t[b])return a[b]=!0}):(n=!1,i=0,layui.each(a[t],function(e,t){t[l]&&i++}),n=a[t].length?a[t].length===i:a[l],a[b]=!n&&0')),n=(e.tree(a),i.elem=p(i.elem));if(n[0]){if(e.key=i.id||e.index,e.elem=a,e.elemNone=p('
      '+i.text.none+"
      "),n.html(e.elem),0==e.elem.find(".layui-tree-set").length)return e.elem.append(e.elemNone);i.showCheckbox&&e.renderForm("checkbox"),e.elem.find(".layui-tree-set").each(function(){var e=p(this);e.parent(".layui-tree-pack")[0]||e.addClass("layui-tree-setHide"),!e.next()[0]&&e.parents(".layui-tree-pack").eq(1).hasClass("layui-tree-lineExtend")&&e.addClass(F),e.next()[0]||e.parents(".layui-tree-set").eq(0).next()[0]||e.addClass(F)}),e.events()}},l.prototype.renderForm=function(e){i.render(e,"LAY-tree-"+this.index)},l.prototype.tree=function(r,e){var d=this,s=d.config,o=s.customName,e=e||s.data;layui.each(e,function(e,i){var a,n,t=i[o.children]&&0"),c=p(['
      ','
      ','
      ',s.showLine?t?'':'':'',s.showCheckbox?'':"",s.isJump&&i.href?''+(i[o.title]||i.label||s.text.defaultNodeName)+"":''+(i[o.title]||i.label||s.text.defaultNodeName)+"","
      ",s.edit?(a={add:'',update:'',del:''},n=['
      '],!0===s.edit&&(s.edit=["update","del"]),"object"==typeof s.edit?(layui.each(s.edit,function(e,i){n.push(a[i]||"")}),n.join("")+"
      "):void 0):"","
      "].join(""));t&&(c.append(l),d.tree(l,i[o.children])),r.append(c),c.prev("."+m)[0]&&c.prev().children(".layui-tree-pack").addClass("layui-tree-showLine"),t||c.parent(".layui-tree-pack").addClass("layui-tree-lineExtend"),d.spread(c,i),s.showCheckbox&&(i.checked&&d.checkids.push(i[o.id]),d.checkClick(c,i)),s.edit&&d.operate(c,i)})},l.prototype.spread=function(n,t){var l=this,c=l.config,e=n.children("."+x),i=e.children("."+b),a=i.find('input[same="layuiTreeCheck"]'),r=e.find("."+k),e=e.find("."+g),d=c.onlyIconControl?r:i,s="";d.on("click",function(e){var i=n.children("."+w),a=(d.children(".layui-icon")[0]?d:d.find(".layui-tree-icon")).children(".layui-icon");i[0]?n.hasClass(N)?(n.removeClass(N),i.slideUp(200),a.removeClass(v).addClass(C),l.updateFieldValue(t,"spread",!1)):(n.addClass(N),i.slideDown(200),a.addClass(v).removeClass(C),l.updateFieldValue(t,"spread",!0),c.accordion&&((i=n.siblings("."+m)).removeClass(N),i.children("."+w).slideUp(200),i.find(".layui-tree-icon").children(".layui-icon").removeClass(v).addClass(C))):s="normal"}),e.on("click",function(){p(this).hasClass(u)||(s=n.hasClass(N)?c.onlyIconControl?"open":"close":c.onlyIconControl?"close":"open",a[0]&&l.updateFieldValue(t,"checked",a.prop("checked")),c.click&&c.click({elem:n,state:s,data:t}))})},l.prototype.updateFieldValue=function(e,i,a){i in e&&(e[i]=a)},l.prototype.setCheckbox=function(e,i,a){var t,n=this,l=n.config.customName,c=a.prop("checked");a.prop("disabled")||("object"!=typeof i[l.children]&&!e.find("."+w)[0]||e.find("."+w).find('input[same="layuiTreeCheck"]').each(function(e){this.disabled||((e=i[l.children][e])&&n.updateFieldValue(e,"checked",c),n.updateFieldValue(this,"checked",c))}),(t=function(e){var i,a,n;e.parents("."+m)[0]&&(a=(e=e.parent("."+w)).parent(),n=e.prev().find('input[same="layuiTreeCheck"]'),c?n.prop("checked",c):(e.find('input[same="layuiTreeCheck"]').each(function(){this.checked&&(i=!0)}),i||n.prop("checked",!1)),t(a))})(e),n.renderForm("checkbox"))},l.prototype.checkClick=function(a,n){var t=this,l=t.config;a.children("."+x).children("."+b).on("click",'input[same="layuiTreeCheck"]+',function(e){layui.stope(e);var e=p(this).prev(),i=e.prop("checked");e.prop("disabled")||(t.setCheckbox(a,n,e),t.updateFieldValue(n,"checked",i),l.oncheck&&l.oncheck({elem:a,checked:i,data:n}))})},l.prototype.operate=function(r,d){var s=this,o=s.config,u=o.customName,e=r.children("."+x),h=e.children("."+b);e.children(".layui-tree-btnGroup").on("click",".layui-icon",function(e){layui.stope(e);var i,e=p(this).data("type"),n=r.children("."+w),t={data:d,type:e,elem:r};if("add"==e){n[0]||(o.showLine?(h.find("."+k).addClass("layui-tree-icon"),h.find("."+k).children(".layui-icon").addClass(C).removeClass("layui-icon-file")):h.find(".layui-tree-iconArrow").removeClass(f),r.append('
      '));var a,l=o.operate&&o.operate(t),c={};if(c[u.title]=o.text.defaultNodeName,c[u.id]=l,s.tree(r.children("."+w),[c]),o.showLine&&(n[0]?(n.hasClass(L)||n.addClass(L),r.find("."+w).each(function(){p(this).children("."+m).last().addClass(F)}),(n.children("."+m).last().prev().hasClass(F)?n.children("."+m).last().prev():n.children("."+m).last()).removeClass(F),!r.parent("."+w)[0]&&r.next()[0]&&n.children("."+m).last().removeClass(F)):(l=r.siblings("."+m),a=1,c=r.parent("."+w),layui.each(l,function(e,i){p(i).children("."+w)[0]||(a=0)}),(1==a?(l.children("."+w).addClass(T),l.children("."+w).children("."+m).removeClass(F),r.children("."+w).addClass(T),c.removeClass(L),c.children("."+m).last().children("."+w).children("."+m).last()):r.children("."+w).children("."+m)).addClass(F))),!o.showCheckbox)return;h.find('input[same="layuiTreeCheck"]')[0].checked&&(r.children("."+w).children("."+m).last().find('input[same="layuiTreeCheck"]')[0].checked=!0),s.renderForm("checkbox")}else"update"==e?(l=h.children("."+g).html(),h.children("."+g).html(""),h.append(''),h.children(".layui-tree-editInput").val(l).focus(),i=function(e){var i=e.val().trim()||o.text.defaultNodeName;e.remove(),h.children("."+g).html(i),t.data[u.title]=i,o.operate&&o.operate(t)},h.children(".layui-tree-editInput").blur(function(){i(p(this))}),h.children(".layui-tree-editInput").on("keydown",function(e){13===e.keyCode&&(e.preventDefault(),i(p(this)))})):y.confirm('\u786e\u8ba4\u5220\u9664\u8be5\u8282\u70b9 "'+(d[u.title]||"")+'" \u5417\uff1f',function(e){var l,a,i;o.operate&&o.operate(t),t.status="remove",y.close(e),r.prev("."+m)[0]||r.next("."+m)[0]||r.parent("."+w)[0]?(r.siblings("."+m).children("."+x)[0]?(o.showCheckbox&&(l=function(e){var i,a,n,t;e.parents("."+m)[0]&&(i=e.siblings("."+m).children("."+x),a=(e=e.parent("."+w).prev()).find('input[same="layuiTreeCheck"]')[0],n=1,(t=0)==a.checked)&&(i.each(function(e,i){i=p(i).find('input[same="layuiTreeCheck"]')[0];0!=i.checked||i.disabled||(n=0),i.disabled||(t=1)}),1==n)&&1==t&&(a.checked=!0,s.renderForm("checkbox"),l(e.parent("."+m)))})(r),o.showLine&&(e=r.siblings("."+m),a=1,i=r.parent("."+w),layui.each(e,function(e,i){p(i).children("."+w)[0]||(a=0)}),1==a?(n[0]||(i.removeClass(L),e.children("."+w).addClass(T),e.children("."+w).children("."+m).removeClass(F)),(r.next()[0]?i.children("."+m).last():r.prev()).children("."+w).children("."+m).last().addClass(F),r.next()[0]||r.parents("."+m)[1]||r.parents("."+m).eq(0).next()[0]||r.prev("."+m).addClass(F)):!r.next()[0]&&r.hasClass(F)&&r.prev().addClass(F))):(e=r.parent("."+w).prev(),o.showLine?(e.find("."+k).removeClass("layui-tree-icon"),e.find("."+k).children(".layui-icon").removeClass(v).addClass("layui-icon-file"),(i=e.parents("."+w).eq(0)).addClass(L),i.children("."+m).each(function(){p(this).children("."+w).children("."+m).last().addClass(F)})):e.find(".layui-tree-iconArrow").addClass(f),r.parents("."+m).eq(0).removeClass(N),r.parent("."+w).remove()),r.remove()):(r.remove(),s.elem.append(s.elemNone))})})},l.prototype.events=function(){var i=this,t=i.config;i.elem.find(".layui-tree-checkedFirst");i.setChecked(i.checkids),i.elem.find(".layui-tree-search").on("keyup",function(){var e=p(this),a=e.val(),e=e.nextAll(),n=[];e.find("."+g).each(function(){var i,e=p(this).parents("."+x);-1!=p(this).html().indexOf(a)&&(n.push(p(this).parent()),(i=function(e){e.addClass("layui-tree-searchShow"),e.parent("."+w)[0]&&i(e.parent("."+w).parent("."+m))})(e.parent("."+m)))}),e.find("."+x).each(function(){var e=p(this).parent("."+m);e.hasClass("layui-tree-searchShow")||e.addClass(f)}),0==e.find(".layui-tree-searchShow").length&&i.elem.append(i.elemNone),t.onsearch&&t.onsearch({elem:n})}),i.elem.find(".layui-tree-search").on("keydown",function(){p(this).nextAll().find("."+x).each(function(){p(this).parent("."+m).removeClass("layui-tree-searchShow "+f)}),p(".layui-tree-emptyText")[0]&&p(".layui-tree-emptyText").remove()})},l.prototype.getChecked=function(){var t=this,e=t.config,l=e.customName,i=[],a=[],c=(t.elem.find(".layui-form-checked").each(function(){i.push(p(this).prev()[0].value)}),function(e,n){layui.each(e,function(e,a){layui.each(i,function(e,i){if(a[l.id]==i)return t.updateFieldValue(a,"checked",!0),delete(i=p.extend({},a))[l.children],n.push(i),a[l.children]&&(i[l.children]=[],c(a[l.children],i[l.children])),!0})})});return c(p.extend({},e.data),a),a},l.prototype.setChecked=function(l){this.config;this.elem.find("."+m).each(function(e,i){var a=p(this).data("id"),n=p(i).children("."+x).find('input[same="layuiTreeCheck"]'),t=n.next();if("number"==typeof l){if(a.toString()==l.toString())return n[0].checked||t.click(),!1}else"object"==typeof l&&layui.each(l,function(e,i){if(i.toString()==a.toString()&&!n[0].checked)return t.click(),!0})})},n.that={},n.config={},t.reload=function(e,i){e=n.that[e];return e.reload(i),n.call(e)},t.getChecked=function(e){return n.that[e].getChecked()},t.setChecked=function(e,i){return n.that[e].setChecked(i)},t.render=function(e){e=new l(e);return n.call(e)},e(a,t)});layui.define(["laytpl","form"],function(e){"use strict";var s=layui.$,n=layui.laytpl,t=layui.form,a="transfer",i={config:{},index:layui[a]?layui[a].index+1e4:0,set:function(e){var t=this;return t.config=s.extend({},t.config,e),t},on:function(e,t){return layui.onevent.call(this,a,e,t)}},l=function(){var t=this,e=t.config,a=e.id||t.index;return l.that[a]=t,{config:l.config[a]=e,reload:function(e){t.reload.call(t,e)},getData:function(){return t.getData.call(t)}}},d="layui-hide",h="layui-btn-disabled",r="layui-none",c="layui-transfer-box",u="layui-transfer-header",o="layui-transfer-search",f="layui-transfer-data",y=function(e){return['
      ','
      ','","
      ","{{# if(d.data.showSearch){ }}",'","{{# } }}",'
        ',"
        "].join("")},p=['
        ',y({index:0,checkAllName:"layTransferLeftCheckAll"}),'
        ','",'","
        ",y({index:1,checkAllName:"layTransferRightCheckAll"}),"
        "].join(""),v=function(e){var t=this;t.index=++i.index,t.config=s.extend({},t.config,i.config,e),t.render()};v.prototype.config={title:["\u5217\u8868\u4e00","\u5217\u8868\u4e8c"],width:200,height:360,data:[],value:[],showSearch:!1,id:"",text:{none:"\u65e0\u6570\u636e",searchNone:"\u65e0\u5339\u914d\u6570\u636e"}},v.prototype.reload=function(e){var t=this;t.config=s.extend({},t.config,e),t.render()},v.prototype.render=function(){var e=this,t=e.config,a=e.elem=s(n(p,{open:"{{",close:"}}"}).render({data:t,index:e.index})),i=t.elem=s(t.elem);i[0]&&(t.data=t.data||[],t.value=t.value||[],t.id="id"in t?t.id:elem.attr("id")||e.index,e.key=t.id,i.html(e.elem),e.layBox=e.elem.find("."+c),e.layHeader=e.elem.find("."+u),e.laySearch=e.elem.find("."+o),e.layData=a.find("."+f),e.layBtn=a.find(".layui-transfer-active .layui-btn"),e.layBox.css({width:t.width,height:t.height}),e.layData.css({height:(i=t.height-e.layHeader.outerHeight(),t.showSearch&&(i-=e.laySearch.outerHeight()),i-2)}),e.renderData(),e.events())},v.prototype.renderData=function(){var e=this,t=e.config,l=[{checkName:"layTransferLeftCheck",views:[]},{checkName:"layTransferRightCheck",views:[]}];e.parseData(function(a){var i=a.selected?1:0,n=["
      • ",'',"
      • "].join("");i?layui.each(t.value,function(e,t){t==a.value&&a.selected&&(l[i].views[e]=n)}):l[i].views.push(n),delete a.selected}),e.layData.eq(0).html(l[0].views.join("")),e.layData.eq(1).html(l[1].views.join("")),e.renderCheckBtn()},v.prototype.renderForm=function(e){t.render(e,"LAY-transfer-"+this.index)},v.prototype.renderCheckBtn=function(r){var c=this,o=c.config;r=r||{},c.layBox.each(function(e){var t=s(this),a=t.find("."+f),t=t.find("."+u).find('input[type="checkbox"]'),i=a.find('input[type="checkbox"]'),n=0,l=!1;i.each(function(){var e=s(this).data("hide");(this.checked||this.disabled||e)&&n++,this.checked&&!e&&(l=!0)}),t.prop("checked",l&&n===i.length),c.layBtn.eq(e)[l?"removeClass":"addClass"](h),r.stopNone||(i=a.children("li:not(."+d+")").length,c.noneView(a,i?"":o.text.none))}),c.renderForm("checkbox")},v.prototype.noneView=function(e,t){var a=s('

        '+(t||"")+"

        ");e.find("."+r)[0]&&e.find("."+r).remove(),t.replace(/\s/g,"")&&e.append(a)},v.prototype.setValue=function(){var e=this.config,t=[];return this.layBox.eq(1).find("."+f+' input[type="checkbox"]').each(function(){s(this).data("hide")||t.push(this.value)}),e.value=t,this},v.prototype.parseData=function(t){var i=this.config,n=[];return layui.each(i.data,function(e,a){a=("function"==typeof i.parseData?i.parseData(a):a)||a,n.push(a=s.extend({},a)),layui.each(i.value,function(e,t){t==a.value&&(a.selected=!0)}),t&&t(a)}),i.data=n,this},v.prototype.getData=function(e){var t=this.config,i=[];return this.setValue(),layui.each(e||t.value,function(e,a){layui.each(t.data,function(e,t){delete t.selected,a==t.value&&i.push(t)})}),i},v.prototype.transfer=function(e,t){var a,i=this,n=i.config,l=i.layBox.eq(e),r=[],t=(t?((a=(t=t).find('input[type="checkbox"]'))[0].checked=!1,l.siblings("."+c).find("."+f).append(t.clone()),t.remove(),r.push(a[0].value),i.setValue()):l.each(function(e){s(this).find("."+f).children("li").each(function(){var e=s(this),t=e.find('input[type="checkbox"]'),a=t.data("hide");t[0].checked&&!a&&(t[0].checked=!1,l.siblings("."+c).find("."+f).append(e.clone()),e.remove(),r.push(t[0].value)),i.setValue()})}),i.renderCheckBtn(),l.siblings("."+c).find("."+o+" input"));""!==t.val()&&t.trigger("keyup"),n.onchange&&n.onchange(i.getData(r),e)},v.prototype.events=function(){var n=this,l=n.config;n.elem.on("click",'input[lay-filter="layTransferCheckbox"]+',function(){var e=s(this).prev(),t=e[0].checked,a=e.parents("."+c).eq(0).find("."+f);e[0].disabled||("all"===e.attr("lay-type")&&a.find('input[type="checkbox"]').each(function(){this.disabled||(this.checked=t)}),setTimeout(function(){n.renderCheckBtn({stopNone:!0})},0))}),n.elem.on("dblclick","."+f+">li",function(e){var t=s(this),a=t.children('input[type="checkbox"]'),i=t.parent().parent();a[0].disabled||n.transfer(i.data("index"),t)}),n.layBtn.on("click",function(){var e=s(this),t=e.data("index");e.hasClass(h)||n.transfer(t)}),n.laySearch.find("input").on("keyup",function(){var i=this.value,e=s(this).parents("."+o).eq(0).siblings("."+f),t=e.children("li"),t=(t.each(function(){var e=s(this),t=e.find('input[type="checkbox"]'),a=t[0].title,a=("cs"!==l.showSearch&&(a=a.toLowerCase(),i=i.toLowerCase()),-1!==a.indexOf(i));e[a?"removeClass":"addClass"](d),t.data("hide",!a)}),n.renderCheckBtn(),t.length===e.children("li."+d).length);n.noneView(e,t?l.text.searchNone:"")})},l.that={},l.config={},i.reload=function(e,t){e=l.that[e];return e.reload(t),l.call(e)},i.getData=function(e){return l.that[e].getData()},i.render=function(e){e=new v(e);return l.call(e)},e(a,i)});layui.define(["jquery","lay"],function(e){"use strict";var a=layui.$,t=layui.lay,o=(layui.hint(),layui.device(),{config:{},set:function(e){var i=this;return i.config=a.extend({},i.config,e),i},on:function(e,i){return layui.onevent.call(this,r,e,i)}}),r="carousel",d="layui-this",s="layui-carousel-left",u="layui-carousel-right",c="layui-carousel-prev",m="layui-carousel-next",l="layui-carousel-arrow",f="layui-carousel-ind",i=function(e){var i=this;i.config=a.extend({},i.config,o.config,e),i.render()};i.prototype.config={width:"600px",height:"280px",full:!1,arrow:"hover",indicator:"inside",autoplay:!0,interval:3e3,anim:"",trigger:"click",index:0},i.prototype.render=function(){var e=this,i=e.config,n=a(i.elem);if(1*[carousel-item]>*"),i.index<0&&(i.index=0),i.index>=e.elemItem.length&&(i.index=e.elemItem.length-1),i.interval<800&&(i.interval=800),i.full?i.elem.css({position:"fixed",width:"100%",height:"100%",zIndex:9999}):i.elem.css({width:i.width,height:i.height}),i.elem.attr("lay-anim",i.anim),e.elemItem.eq(i.index).addClass(d),e.elemItem.length<=1||(e.indicator(),e.arrow(),e.autoplay(),e.events()))},i.prototype.reload=function(e){var i=this;clearInterval(i.timer),i.config=a.extend({},i.config,e),i.render()},i.prototype.prevIndex=function(){var e=this.config.index-1;return e=e<0?this.elemItem.length-1:e},i.prototype.nextIndex=function(){var e=this.config.index+1;return e=e>=this.elemItem.length?0:e},i.prototype.addIndex=function(e){var i=this.config;i.index=i.index+(e=e||1),i.index>=this.elemItem.length&&(i.index=0)},i.prototype.subIndex=function(e){var i=this.config;i.index=i.index-(e=e||1),i.index<0&&(i.index=this.elemItem.length-1)},i.prototype.autoplay=function(){var e=this,i=e.config;i.autoplay&&(clearInterval(e.timer),e.timer=setInterval(function(){e.slide()},i.interval))},i.prototype.arrow=function(){var i=this,e=i.config,n=a(['",'"].join(""));e.elem.attr("lay-arrow",e.arrow),e.elem.find("."+l)[0]&&e.elem.find("."+l).remove(),e.elem.append(n),n.on("click",function(){var e=a(this).attr("lay-type");i.slide(e)})},i.prototype["goto"]=function(e){var i=this,n=i.config;e>n.index?i.slide("add",e-n.index):e
          ',(i=[],layui.each(e.elemItem,function(e){i.push("")}),i.join("")),"
        "].join(""));n.elem.attr("lay-indicator",n.indicator),n.elem.find("."+f)[0]&&n.elem.find("."+f).remove(),n.elem.append(t),"updown"===n.anim&&t.css("margin-top",-t.height()/2),t.find("li").on("hover"===n.trigger?"mouseover":n.trigger,function(){e["goto"](a(this).index())})},i.prototype.slide=function(e,i){var n=this,t=n.elemItem,a=n.config,o=a.index,l=a.elem.attr("lay-filter");n.haveSlide||("sub"===e?(n.subIndex(i),t.eq(a.index).addClass(c),setTimeout(function(){t.eq(o).addClass(u),t.eq(a.index).addClass(u)},50)):(n.addIndex(i),t.eq(a.index).addClass(m),setTimeout(function(){t.eq(o).addClass(s),t.eq(a.index).addClass(s)},50)),setTimeout(function(){t.removeClass(d+" "+c+" "+m+" "+s+" "+u),t.eq(a.index).addClass(d),n.haveSlide=!1},350),n.elemInd.find("li").eq(a.index).addClass(d).siblings().removeClass(d),n.haveSlide=!0,e={index:a.index,prevIndex:o,item:t.eq(a.index)},"function"==typeof a.change&&a.change(e),layui.event.call(this,r,"change("+l+")",e))},i.prototype.events=function(){var e=this,i=e.config;i.elem.data("haveEvents")||(i.elem.on("mouseenter",function(){"always"!==e.config.autoplay&&clearInterval(e.timer)}).on("mouseleave",function(){"always"!==e.config.autoplay&&e.autoplay()}),i.elem.data("haveEvents",!0))},o.render=function(e){return new i(e)},e(r,o)});layui.define(["jquery","lay"],function(e){"use strict";var s=layui.jquery,r=layui.lay,c={config:{},index:layui.rate?layui.rate.index+1e4:0,set:function(e){var a=this;return a.config=s.extend({},a.config,e),a},on:function(e,a){return layui.onevent.call(this,l,e,a)}},l="rate",f="layui-icon-rate",h="layui-icon-rate-solid",o="layui-icon-rate-half",u="layui-icon-rate-solid layui-icon-rate-half",v="layui-icon-rate layui-icon-rate-half",a=function(e){var a=this;a.index=++c.index,a.config=s.extend({},a.config,c.config,e),a.render()};a.prototype.config={length:5,text:!1,readonly:!1,half:!1,value:0,theme:""},a.prototype.render=function(){var e=this,a=e.config,l=s(a.elem);if(1a.length&&(a.value=a.length),parseInt(a.value)===a.value||a.half||(a.value=Math.ceil(a.value)-a.value<.5?Math.ceil(a.value):Math.floor(a.value)),'
          "),t=1;t<=a.length;t++){var o='
        • ";a.half&&parseInt(a.value)!==a.value&&t==Math.ceil(a.value)?n=n+'
        • ":n+=o}n+="
        "+(a.text?''+a.value+"\u661f":"")+"";var l=a.elem,u=l.next(".layui-rate");u[0]&&u.remove(),e.elemTemp=s(n),a.span=e.elemTemp.next("span"),a.setText&&a.setText(a.value),l.html(e.elemTemp),l.addClass("layui-inline"),a.readonly||e.action()},a.prototype.setvalue=function(e){this.config.value=e,this.render()},a.prototype.action=function(){var i=this.config,n=this.elemTemp,t=n.find("i").width();n.children("li").each(function(e){var a=e+1,l=s(this);l.on("click",function(e){i.value=a,i.half&&e.pageX-s(this).offset().left<=t/2&&(i.value=i.value-.5),i.text&&n.next("span").text(i.value+"\u661f"),i.choose&&i.choose(i.value),i.setText&&i.setText(i.value)}),l.on("mousemove",function(e){n.find("i").each(function(){s(this).addClass(f).removeClass(u)}),n.find("i:lt("+a+")").each(function(){s(this).addClass(h).removeClass(v)}),i.half&&e.pageX-s(this).offset().left<=t/2&&l.children("i").addClass(o).removeClass(h)}),l.on("mouseleave",function(){n.find("i").each(function(){s(this).addClass(f).removeClass(u)}),n.find("i:lt("+Math.floor(i.value)+")").each(function(){s(this).addClass(h).removeClass(v)}),i.half&&parseInt(i.value)!==i.value&&n.children("li:eq("+Math.floor(i.value)+")").children("i").addClass(o).removeClass("layui-icon-rate-solid layui-icon-rate")})})},a.prototype.events=function(){},c.render=function(e){e=new a(e);return function(){var a=this;return{setvalue:function(e){a.setvalue.call(a,e)},config:a.config}}.call(e)},e(l,c)});layui.define("jquery",function(l){"use strict";var g=layui.$,e=function(l){};e.prototype.load=function(l){var t,i,o,n,e,r,a,c,m,s,u,f,y,d=this,p=0,h=g((l=l||{}).elem);if(h[0])return e=g(l.scrollElem||document),r=l.mb||50,a=!("isAuto"in l)||l.isAuto,c=l.end||"\u6ca1\u6709\u66f4\u591a\u4e86",m=l.scrollElem&&l.scrollElem!==document,u=g('"),h.find(".layui-flow-more")[0]||h.append(u),f=function(l,e){l=g(l),u.before(l),(e=0==e||null)?u.html(c):u.find("a").html(s),i=e,t=null,o&&o()},(y=function(){t=!0,u.find("a").html(''),"function"==typeof l.done&&l.done(++p,f)})(),u.find("a").on("click",function(){g(this);i||t||y()}),l.isLazyimg&&(o=d.lazyimg({elem:l.elem+" img",scrollElem:l.scrollElem})),a&&e.on("scroll",function(){var e=g(this),o=e.scrollTop();n&&clearTimeout(n),!i&&h.width()&&(n=setTimeout(function(){var l=(m?e:g(window)).height();(m?e.prop("scrollHeight"):document.documentElement.scrollHeight)-o-l<=r&&(t||y())},100))}),d},e.prototype.lazyimg=function(l){var e,c=this,m=0,s=g((l=l||{}).scrollElem||document),u=l.elem||"img",f=l.scrollElem&&l.scrollElem!==document,y=function(e,l){var o,t=s.scrollTop(),l=t+l,i=f?e.offset().top-s.offset().top+t:e.offset().top;t<=i&&i<=l&&e.attr("lay-src")&&(o=e.attr("lay-src"),layui.img(o,function(){var l=c.lazyimg.elem.eq(m);e.attr("src",o).removeAttr("lay-src"),l[0]&&n(l),m++},function(){c.lazyimg.elem.eq(m);e.removeAttr("lay-src")}))},n=function(l,e){var o=(f?e||s:g(window)).height(),t=s.scrollTop(),i=t+o;if(c.lazyimg.elem=g(u),l)y(l,o);else for(var n=0;n"),preview:"Preview"},wordWrap:!0,lang:"text",highlighter:!1,langMarker:!1},W=layui.code?layui.code.index+1e4:0,R=function(e){return String(e).replace(/\s+$/,"").replace(/^\n|\n$/,"")};e("code",function(l,e){var o,i,t,a,n,d,c,s,r,u,y,p,E,f,h,v,m,L,_,M,C,g={config:l=x.extend(!0,{},T,l),reload:function(e){layui.code(this.updateOptions(e))},updateOptions:function(e){return delete(e=e||{}).elem,x.extend(!0,l,e)},reloadCode:function(e){layui.code(this.updateOptions(e),"reloadCode")}},w=x(l.elem);return 1',l.ln?['
        ',D.digit(t+1)+".","
        "].join(""):"",'
        ',e||" ","
        ",""].join("")})}},a=l.code,n=function(e){return"function"==typeof l.codeParse?l.codeParse(e,l):e},"reloadCode"===e?o.children(".layui-code-wrap").html(w(n(a)).html):(d=layui.code.index=++W,o.attr("lay-code-index",d),(M=A.CDDE_DATA_CLASS in o.data())&&o.attr("class",o.data(A.CDDE_DATA_CLASS)||""),M||o.data(A.CDDE_DATA_CLASS,o.attr("class")),c={copy:{className:"file-b",title:["\u590d\u5236\u4ee3\u7801"],event:function(e){var t=D.unescape(n(l.code));lay.clipboard.writeText({text:t,done:function(){N.msg("\u5df2\u590d\u5236",{icon:1})},error:function(){N.msg("\u590d\u5236\u5931\u8d25",{icon:2})}}),"function"==typeof l.onCopy&&l.onCopy(t)}}},function b(){var e=o.parent("."+A.ELEM_PREVIEW),t=e.children("."+A.ELEM_TAB),a=e.children("."+A.ELEM_ITEM+"-preview");return t.remove(),a.remove(),e[0]&&o.unwrap(),b}(),l.preview&&(M="LAY-CODE-DF-"+d,f=l.layout||["code","preview"],s="iframe"===l.preview,E=x('
        '),C=x('
        '),r=x('
        '),_=x('
        '),u=x('
        '),l.id&&E.attr("id",l.id),E.addClass(l.className),C.attr("lay-filter",M),layui.each(f,function(e,t){var a=x('
      • ');0===e&&a.addClass("layui-this"),a.html(l.text[t]),r.append(a)}),x.extend(c,{full:{className:"screen-full",title:["\u6700\u5927\u5316\u663e\u793a","\u8fd8\u539f\u663e\u793a"],event:function(e){var e=e.elem,t=e.closest("."+A.ELEM_PREVIEW),a="layui-icon-"+this.className,i="layui-icon-screen-restore",l=this.title,o=x("html,body"),n="layui-scrollbar-hide";e.hasClass(a)?(t.addClass(A.ELEM_FULL),e.removeClass(a).addClass(i),e.attr("title",l[1]),o.addClass(n)):(t.removeClass(A.ELEM_FULL),e.removeClass(i).addClass(a),e.attr("title",l[0]),o.removeClass(n))}},window:{className:"release",title:["\u5728\u65b0\u7a97\u53e3\u9884\u89c8"],event:function(e){D.openWin({content:n(l.code)})}}}),l.copy&&("array"===layui.type(l.tools)?-1===l.tools.indexOf("copy")&&l.tools.unshift("copy"):l.tools=["copy"]),u.on("click",">i",function(){var e=x(this),t=e.data("type"),e={elem:e,type:t,options:l,rawCode:l.code,finalCode:D.unescape(n(l.code))};c[t]&&"function"==typeof c[t].event&&c[t].event(e),"function"==typeof l.toolsEvent&&l.toolsEvent(e)}),l.addTools&&l.tools&&(l.tools=[].concat(l.tools,l.addTools)),layui.each(l.tools,function(e,t){var a="object"==typeof t,i=a?t:c[t]||{className:t,title:[t]},l=i.className||i.type,o=i.title||[""],a=a?i.type||l:t;a&&(c[a]||((t={})[a]=i,x.extend(c,t)),u.append(''))}),o.addClass(A.ELEM_ITEM).wrap(E),C.append(r),l.tools&&C.append(u),o.before(C),s&&_.html(''),y=function(e){var t=e.children("iframe")[0];s&&t?t.srcdoc=n(l.code):e.html(l.code),setTimeout(function(){"function"==typeof l.done&&l.done({container:e,options:l,render:function(){I.render(e.find(".layui-form")),S.render()}})},3)},"preview"===f[0]?(_.addClass(A.ELEM_SHOW),o.before(_),y(_)):o.addClass(A.ELEM_SHOW).after(_),l.previewStyle=[l.style,l.previewStyle].join(""),_.attr("style",l.previewStyle),S.on("tab("+M+")",function(e){var t=x(this),a=x(e.elem).closest("."+A.ELEM_PREVIEW).find("."+A.ELEM_ITEM),e=a.eq(e.index);a.removeClass(A.ELEM_SHOW),e.addClass(A.ELEM_SHOW),"preview"===t.attr("lay-id")&&y(e),L()})),p=x(''),o.addClass((E=["layui-code-view layui-border-box"],l.wordWrap||E.push("layui-code-nowrap"),E.join(" "))),(C=l.theme||l.skin)&&(o.removeClass("layui-code-theme-dark layui-code-theme-light"),o.addClass("layui-code-theme-"+C)),l.highlighter&&o.addClass([l.highlighter,"language-"+l.lang,"layui-code-hl"].join(" ")),f=w(l.encode?D.escape(n(a)):a),h=f.lines,o.html(p.html(f.html)),l.ln&&o.append('
        '),l.height&&p.css("max-height",l.height),l.codeStyle=[l.style,l.codeStyle].join(""),l.codeStyle&&p.attr("style",function(e,t){return(t||"")+l.codeStyle}),v=[{selector:">.layui-code-wrap>.layui-code-line{}",setValue:function(e,t){e.style["padding-left"]=t+"px"}},{selector:">.layui-code-wrap>.layui-code-line>.layui-code-line-number{}",setValue:function(e,t){e.style.width=t+"px"}},{selector:">.layui-code-ln-side{}",setValue:function(e,t){e.style.width=t+"px"}}],m=lay.style({target:o[0],id:"DF-code-"+d,text:x.map(x.map(v,function(e){return e.selector}),function(e,t){return['.layui-code-view[lay-code-index="'+d+'"]',e].join(" ")}).join("")}),L=function b(){var e,i;return l.ln&&(e=Math.floor(h.length/100),i=p.children("."+A.ELEM_LINE).last().children("."+A.ELEM_LINE_NUM).outerWidth(),o.addClass(A.ELEM_LN_MODE),e)&&A.LINE_RAW_WIDTH
      • ')).html(l.title||l.text.code),o.prepend(_)),M=x('
        '),l.copy&&!l.preview&&((C=x(['','',""].join(""))).on("click",function(){c.copy.event()}),M.append(C)),l.langMarker&&M.append(''+l.lang+""),l.about&&M.append(l.about),o.append(M),l.preview||setTimeout(function(){"function"==typeof l.done&&l.done({})},3),l.elem.length===1+d&&"function"==typeof l.allDone&&l.allDone())),g})}),layui["layui.all"]||layui.addcss("modules/code.css?v=6","skincodecss"); \ No newline at end of file diff --git a/src/main/resources/style/index.css b/src/main/resources/style/index.css new file mode 100644 index 0000000..42e810c --- /dev/null +++ b/src/main/resources/style/index.css @@ -0,0 +1,9 @@ +.layui-badge-rim, .layui-border, .layui-colla-content, .layui-colla-item, .layui-collapse, .layui-elem-field, .layui-form-pane .layui-form-item[pane], .layui-form-pane .layui-form-label, .layui-input, .layui-input-split, .layui-panel, .layui-quote-nm, .layui-select, .layui-tab-bar, .layui-tab-card, .layui-tab-title, .layui-tab-title .layui-this:after, .layui-textarea { + border-color: #a9a9a9; +} +.layui-form-label { + padding-left: 0; +} +.layui-input-block { + margin-left: 95px; +} diff --git a/src/main/resources/template/index.html b/src/main/resources/template/index.html new file mode 100644 index 0000000..e93bbaf --- /dev/null +++ b/src/main/resources/template/index.html @@ -0,0 +1,80 @@ + + + + + EagleEye注册码生成器 + + + + +
        +
        +
        + +
        + +
        +
        +
        + +
        + +
        +
        + + + + +
        +
        +
        + +
        + +
        +
        +
        + +
        +
        +
        + + + + \ No newline at end of file